package org.intrace.output;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
//import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TimeUnit;
import org.intrace.agent.server.AgentClientConnection;
import org.intrace.shared.SerializationHelper;
public class NetworkDataSenderThread extends InstruRunnable {
private long traceEventSequence = 0;
private static final int EST_EVENT_COUNT_PER_BATCH = 16 * 1024;
private static final int BURST_SIZE = 16 * 1024;
private static final int DRAIN_INTERVAL_MS = 1000;
/**
* All BatchQueueEle instances will point to this singleton, to avoid
* unnecessary GC overhead. I made this public so that in the future, the
* parameters could be changed on the fly.
*/
public static IBatchSchedulerConfig batchSchedulerConfig = new IBatchSchedulerConfig() {
@Override
public int getDrainInterval() {
return DRAIN_INTERVAL_MS;
}
@Override
public int getDrainIntervalMultiplier() {
return 1;
}
};
/**
* An internal wrapper class for an individual trace event. Enables use of
* java.util.concurrent.DelayQueue to schedule multiples of these to be
* shipped to the client in a single batch, to avoid previous chatty and
* poorly performing behavior of a single socket write per trace event.
*
* Because of the excess GC required, I'm not thrilled about needing this
* wrapper instance for every trace event. However, the DelayQueue solution
* is otherwise pretty elegant.
*
* @author erikostermueller
*
*/
public static class TraceEventForBatch implements Delayed {
private Object traceEventText = null;
private long arrivalTimeMillis = 0L;
private long traceEventSequence = 0L;
private BatchScheduler batchScheduler = null;
public TraceEventForBatch(Object val, long arrivalTimeMillis,
long traceEventSequence) {
this.traceEventText = val;
this.batchScheduler = new BatchScheduler(arrivalTimeMillis,
batchSchedulerConfig);
this.traceEventSequence = traceEventSequence;
}
public Object getTraceEvent() {
return this.traceEventText;
}
@Override
public int compareTo(Delayed o) {
long rc = this.getDelay(TimeUnit.MILLISECONDS)
- o.getDelay(TimeUnit.MILLISECONDS);
return (int) rc;
}
public long getTraceEventSequence() {
return this.traceEventSequence;
}
/**
* "Expiration occurs when an element's getDelay(TimeUnit.NANOSECONDS) method returns a value less than or equal to zero."
* ....taken from here:
* https://docs.oracle.com/javase/7/docs/api/java/util
* /concurrent/DelayQueue.html
*/
@Override
public long getDelay(TimeUnit unit) {
long delay = this.batchScheduler.getDepartureTimeMillis()
- System.currentTimeMillis();
return unit.convert(delay, TimeUnit.MILLISECONDS);
}
}
private boolean alive = true;
private final ServerSocket networkSocket;
private Socket traceSendingSocket = null;
// private final BlockingQueue<Object> outgoingData = new
// LinkedTransferQueue<Object>();
private final BlockingQueue<TraceEventForBatch> outgoingData = new DelayQueue<TraceEventForBatch>();
private Map<NetworkDataSenderThread, Object> set = new HashMap<NetworkDataSenderThread, Object>();
private final AgentClientConnection connection;
public NetworkDataSenderThread(AgentClientConnection connection,
ServerSocket networkSocket) {
this.connection = connection;
this.networkSocket = networkSocket;
}
public void start(Map<NetworkDataSenderThread, Object> set) {
this.set = set;
Thread networkThread = new Thread(this);
networkThread.setDaemon(true);
networkThread.setName(Thread.currentThread().getName()
+ " - Network Data Sender");
networkThread.start();
}
private void stop() {
try {
if (connection != null) {
connection.setTraceConnEstablished(false);
}
alive = false;
networkSocket.close();
if (traceSendingSocket != null) {
traceSendingSocket.close();
}
} catch (IOException e) {
// Throw away
}
set.remove(this);
// System.out.println("## Trace Connection Disconnected");
}
public void queueData(Object data) {
TraceEventForBatch dataWrapper = new TraceEventForBatch(data,
System.currentTimeMillis(), traceEventSequence++);
if (alive)
outgoingData.offer(dataWrapper);
}
public void runMethod_OLD() {
final int HEARTBEAT_TIME_SECONDS = 5;
try {
traceSendingSocket = networkSocket.accept();
// System.out.println("## Trace Connection Established");
traceSendingSocket.setKeepAlive(true);
if (connection != null) {
connection.setTraceConnEstablished(true);
}
ObjectOutputStream traceWriter = new ObjectOutputStream(
traceSendingSocket.getOutputStream());
// Ready to handle data
set.put(this, new Object());
List<String> traceEventsForSingleBatch = new ArrayList<String>(
EST_EVENT_COUNT_PER_BATCH);
List<TraceEventForBatch> tmp = new ArrayList<TraceEventForBatch>(
EST_EVENT_COUNT_PER_BATCH);
while (true) {
TraceEventForBatch traceLineWrapper = outgoingData.poll(
HEARTBEAT_TIME_SECONDS, TimeUnit.SECONDS);
if (traceLineWrapper != null) {
if (traceLineWrapper.getTraceEvent() instanceof String) {
traceEventsForSingleBatch.clear();
tmp.clear();
traceEventsForSingleBatch.add((String) traceLineWrapper
.getTraceEvent());
outgoingData.drainTo(tmp, BURST_SIZE);
Collections.sort(tmp,
new Comparator<TraceEventForBatch>() { // It all
// ends
// badly
// without
// this
// sort.
@Override
public int compare(TraceEventForBatch o1,
TraceEventForBatch o2) {
return (int) (o1
.getTraceEventSequence() - o2
.getTraceEventSequence());
}
});
/**
* This copy from-TraceEventForBatch-to-string-array is
* required to avoid 2 things: -- wire format where each
* trace event has the extra overhead/space of one long
* timestamp (the one used for DelayQueue.getDelay() ).
* -- Requiring TraceEventForBatch .class to be in
* client JVM. This will avoid versioning conflicts.
*/
for (TraceEventForBatch tefb : tmp)
traceEventsForSingleBatch.add((String) tefb
.getTraceEvent());
String[] eventsForOneBatch = new String[traceEventsForSingleBatch
.size()];
traceEventsForSingleBatch.toArray(eventsForOneBatch);
byte[] wireData = SerializationHelper
.toWire(eventsForOneBatch);
traceWriter.writeObject(wireData);
}
} else {
traceWriter.writeObject("NOOP"); // If no events in the last
// HEARTBEAT_TIME_SECONDS,
// then send a NOOP to
// keep the tcp
// connection alive.
}
traceWriter.flush();
traceWriter.reset();
}
} catch (InterruptedException ex) {
stop();
} catch (IOException ex) {
stop();
}
}
public void runMethod() {
final int HEARTBEAT_TIME_SECONDS = 5;
try {
traceSendingSocket = networkSocket.accept();
// System.out.println("## Trace Connection Established");
traceSendingSocket.setKeepAlive(true);
if (connection != null) {
connection.setTraceConnEstablished(true);
}
ObjectOutputStream traceWriter = new ObjectOutputStream(
traceSendingSocket.getOutputStream());
// Ready to handle data
set.put(this, new Object());
while (true) {
TraceEventForBatch traceLineWrapper = outgoingData.poll(
HEARTBEAT_TIME_SECONDS, TimeUnit.SECONDS);
if (traceLineWrapper != null) {
if (traceLineWrapper.getTraceEvent() instanceof String) {
List<TraceEventForBatch> typedAndSortedList = new ArrayList<TraceEventForBatch>(
EST_EVENT_COUNT_PER_BATCH);
typedAndSortedList.add(traceLineWrapper);
while (outgoingData.drainTo(typedAndSortedList, BURST_SIZE) > 0)
;
// It all ends badly without this sort
Collections.sort(typedAndSortedList,
new Comparator<TraceEventForBatch>() {
@Override
public int compare(TraceEventForBatch o1,
TraceEventForBatch o2) {
return (int) (o1
.getTraceEventSequence() - o2
.getTraceEventSequence());
}
});
transmitBatch(traceWriter, typedAndSortedList, BURST_SIZE);
} else {
throw new RuntimeException(
"Found unsupported trace event of type ["
+ traceLineWrapper.getTraceEvent()
.getClass().getName() + "]");
}
} else {
traceWriter.writeObject("NOOP"); // If no events in the last
// HEARTBEAT_TIME_SECONDS,
// then send a NOOP to
// keep the tcp
// connection alive.
}
traceWriter.flush();
traceWriter.reset();
}
} catch (InterruptedException ex) {
stop();
} catch (IOException ex) {
stop();
}
}
/**
* Using the give ObjectOutputStream, transmit the given list of events
* in one or more successive bursts of no more than burstSize events.
*
* This copy from-TraceEventForBatch-to-string-array is required to
* avoid 2 things:
* -- Wire format where each trace event has the extra
* overhead/space of one long timestamp (the one used for
* DelayQueue.getDelay() ).
* -- More complicated wire format that will
* diminish the effectiveness of compression
* -- Requiring TraceEventForBatch .class to be in client JVM.
* This will avoid versioning conflicts.
* @param traceWriter
* @param sortedAndTypedEventList
* @throws IOException
*/
public int transmitBatch(ObjectOutputStream traceWriter,
List<TraceEventForBatch> sortedAndTypedEventList, int burstSize)
throws IOException {
List<String> eventsForSingleBurst = new ArrayList<String>(
burstSize);
int remaining = sortedAndTypedEventList.size();
int burstCount = 0;
for (TraceEventForBatch tefb : sortedAndTypedEventList) {
eventsForSingleBurst.add( (String) tefb.getTraceEvent() );
remaining--;
if (eventsForSingleBurst.size() % burstSize == 0
|| remaining ==0 ) {
String[] aryEventsForSingleBurst = new String[eventsForSingleBurst.size()];
eventsForSingleBurst.toArray(aryEventsForSingleBurst);
byte[] wireData = SerializationHelper.toWire(aryEventsForSingleBurst);
traceWriter.writeObject(wireData);
burstCount++;
eventsForSingleBurst.clear();
}
}
System.out.println("stranded items [" + remaining + "]");
return burstCount;
}
public void gracefulShutdown() {
while (!outgoingData.isEmpty()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// Ignore
}
}
}
}