package org.greg.client;
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.GZIPOutputStream;
public class Greg {
private static final ConcurrentLinkedQueue<Record> records = new ConcurrentLinkedQueue<Record>();
private static final AtomicInteger numDropped = new AtomicInteger(0);
// Don't use ConcurrentLinkedQueue.size() because it's O(n)
private static final AtomicInteger numRecords = new AtomicInteger(0);
private static final Configuration conf = Configuration.INSTANCE;
private static final UUID OUR_UUID = UUID.randomUUID();
private static final String hostname;
static {
Thread pushMessages = new Thread("GregPushMessages") {
public void run() {
pushCurrentMessages();
}
};
pushMessages.setDaemon(true);
pushMessages.start();
Thread initCalibration = new Thread("GregInitiateCalibration") {
public void run() {
initiateCalibration();
}
};
initCalibration.setDaemon(true);
initCalibration.start();
try {
hostname = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
throw new AssertionError("Can't get localhost?");
}
}
public static void log(String message) {
if (numRecords.get() < conf.maxBufferedRecords) {
numRecords.incrementAndGet();
Record r = new Record();
r.message = message;
r.timestamp = PreciseClock.INSTANCE.now();
int prevNumDropped = numDropped.getAndSet(0);
if (prevNumDropped > 0) {
Trace.writeLine("Stopped dropping messages, " + prevNumDropped + " were dropped");
}
records.offer(r);
} else {
int newNumDropped = numDropped.incrementAndGet();
if (newNumDropped == 0) {
Trace.writeLine("Starting to drop messages because of full queue");
} else if (newNumDropped % 100000 == 0) {
Trace.writeLine(newNumDropped + " dropped in a row...");
}
}
}
private static void pushCurrentMessages() {
while (true) {
while (records.isEmpty()) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
continue;
}
}
boolean exhausted = true;
Socket client = null;
OutputStream bStream = null;
OutputStream stream = null;
try {
client = new Socket(conf.server, conf.port);
Trace.writeLine(
"Client connected to " + client.getRemoteSocketAddress() +
" from " + client.getLocalSocketAddress());
bStream = new BufferedOutputStream(client.getOutputStream(), 65536);
DataOutput w = new LittleEndianDataOutputStream(bStream);
w.writeLong(OUR_UUID.getLeastSignificantBits());
w.writeLong(OUR_UUID.getMostSignificantBits());
w.writeBoolean(conf.useCompression);
stream = new BufferedOutputStream(conf.useCompression ? new GZIPOutputStream(bStream) : bStream, 65536);
exhausted = writeRecordsBatchTo(stream);
} catch (Exception e) {
Trace.writeLine("Failed to push messages: " + e);
// Ignore: logging is not *that* important and we're not a persistent message queue.
// Perhaps better luck during the next iteration.
} finally {
close(stream);
close(bStream);
close(client);
}
// Only sleep when waiting for new records.
if (exhausted) {
try {
Thread.sleep(conf.flushPeriodMs);
} catch (InterruptedException e) {
continue;
}
}
}
}
private static void close(Closeable c) {
if (c != null) {
try {
c.close();
} catch (IOException e) {
// Ignore
}
}
}
private static void close(Socket sock) {
if (sock != null) {
try {
sock.close();
} catch (IOException e) {
// Ignore
}
}
}
private static boolean writeRecordsBatchTo(OutputStream stream) throws IOException {
int maxBatchSize = 10000;
DataOutput w = new LittleEndianDataOutputStream(stream);
byte[] cidBytes = conf.clientId.getBytes("utf-8");
w.writeInt(cidBytes.length);
w.write(cidBytes);
int recordsWritten = 0;
byte[] machineBytes = hostname.getBytes("utf-8");
CharsetEncoder enc = Charset.forName("utf-8").newEncoder();
ByteBuffer maxMsg = ByteBuffer.allocate(1);
for(Record rec : records) {
w.writeInt(1);
w.writeLong(rec.timestamp.toUtcNanos());
w.writeInt(machineBytes.length);
w.write(machineBytes);
int maxLen = rec.message.length() * 2;
if(maxLen > maxMsg.limit()) {
maxMsg = ByteBuffer.allocate(maxLen);
}
enc.reset();
enc.encode(CharBuffer.wrap(rec.message), maxMsg, true);
maxMsg.position(0);
w.writeInt(maxMsg.limit());
w.write(maxMsg.array(), maxMsg.arrayOffset(), maxMsg.limit());
if(++recordsWritten == maxBatchSize)
break;
}
w.writeInt(0);
// Only remove records once we're sure that they have been written to server (no exception happened to this point)
stream.flush();
for(int i = 0; i < recordsWritten; ++i) {
records.remove();
}
numRecords.addAndGet(-recordsWritten);
Trace.writeLine("Written batch of " + recordsWritten + " records to greg");
return recordsWritten < maxBatchSize;
}
private static void initiateCalibration() {
while (true) {
Socket client = null;
try {
client = new Socket(conf.server, conf.calibrationPort);
client.setTcpNoDelay(true);
exchangeTicksOver(client.getInputStream(), client.getOutputStream());
} catch (Exception e) {
Trace.writeLine("Failed to exchange clock ticks during calibration, ignoring" + e);
} finally {
close(client);
}
try {
Thread.sleep(conf.calibrationPeriodSec * 1000L);
} catch (InterruptedException e) {
continue;
}
}
}
private static void exchangeTicksOver(InputStream in, OutputStream out) throws IOException {
DataOutput w = new LittleEndianDataOutputStream(out);
DataInput r = new LittleEndianDataInputStream(in);
w.writeLong(OUR_UUID.getLeastSignificantBits());
w.writeLong(OUR_UUID.getMostSignificantBits());
while (true) {
// Here they measure their time and send it to us. It arrives after network latency.
try {
r.readLong(); // Their ticks
} catch (EOFException e) {
break;
}
w.writeLong(PreciseClock.INSTANCE.now().toUtcNanos());
// Our sample arrives to them after network latency.
}
}
}