CoralSequencer uses its own binary and garbage-free serialization framework to read and write its internal messages. For your application messages, you are free to use any serialization library or binary data model you choose. The fact that CoralSequencer is message agnostic gives you total flexibility in that decision. But you can also consider using CoralSequencer’s native serialization framework described in this article.
To define the schema of a message you simply inherit from AbstractProto
and define the message data fields. Below a self-explanatory example:
import com.coralblocks.coralsequencer.protocol.AbstractProto; import com.coralblocks.coralsequencer.protocol.field.*; public class OrderNew extends AbstractProto { private static final int SYMBOL_LENGTH = 8; public static final char TYPE = 'O'; public static final char SUBTYPE = 'N'; public final TypeField typeField = new TypeField(this, TYPE); public final SubtypeField subtypeField = new SubtypeField(this, SUBTYPE); public final CharsField symbol = new CharsField(this, SYMBOL_LENGTH); public final CharField side = new CharField(this); public final LongField size = new LongField(this); public final LongField price = new LongField(this); public final LongField myTimestamp = new LongField(this); public final LongField splitTimestamp = new LongField(this); public final BooleanField isLastChild = new BooleanField(this); }
To send out the OrderNew
message above, you can simply re-use the same OrderNew
instance, over and over again, creating zero garbage. Below an example of how you would send an OrderNew
message from a CoralSequencer node:
if (topBook.isSignal.get()) { long splitTimestamp = useEpoch ? timestamper.nanoEpoch() : timestamper.nanoTime(); for(int i = 0; i < ordersToSend; i++) { boolean isBid = (i % 2 == 0); orderNew.symbol.set(topBook.symbol.get()); if (isBid) { orderNew.side.set('B'); orderNew.size.set(topBook.bidSize.get()); orderNew.price.set(topBook.bidPrice.get()); } else { orderNew.side.set('S'); orderNew.size.set(topBook.askSize.get()); orderNew.price.set(topBook.askPrice.get()); } orderNew.myTimestamp.set(useEpoch ? timestamper.nanoEpoch() : timestamper.nanoTime()); orderNew.splitTimestamp.set(splitTimestamp); orderNew.isLastChild.set(i == ordersToSend - 1); if (batching) { writeCommand(orderNew); } else { sendCommand(orderNew); } } if (batching) flush(); }
As you can see, you simply populate the fields with data and call the sendCommand(Proto)
method of a CoralSequencer node.
Now to receive a CoralSequencer Proto message, you first need to define a parser for your Proto messages. Luckily that’s super easy as you can see below:
import com.coralblocks.coralsequencer.protocol.AbstractMessageProtoParser; import com.coralblocks.coralsequencer.protocol.Proto; public class ProtoParser extends AbstractMessageProtoParser { @Override protected Proto[] createProtoMessages() { return new Proto[] { new OrderNew(), new TopBook(), new OrderCancel() }; } }
Then you can use the proto parser above inside your Node’s handleMessage method to parse a Proto message out of a CoralSequencer message:
private final ProtoParser protoParser = new ProtoParser(); @Override protected void handleMessage(Message msg) { if (isRewinding()) return; // do nothing during rewind... char type = protoParser.getType(msg); char subtype = protoParser.getSubtype(msg); if (type == OrderNew.TYPE && subtype == OrderNew.SUBTYPE) { OrderNew orderNew = (OrderNew) protoParser.parse(msg); if (orderNew == null) { Error.log(name, "Can't parse OrderNew:", msg.toCharSequence()); return; } long now = useEpoch ? timestamper.nanoEpoch() : timestamper.nanoTime(); long latency = now - orderNew.myTimestamp.get(); ordersBench.measure(latency); if (orderNew.isLastChild.get()) { latency = now - orderNew.splitTimestamp.get(); splitBench.measure(latency); } } }
Cool! That’s great! So what is the downside of using CoralSequencer’s serialization framework? To keep things simple, super fast and garbage-free, it does not give you any help for schema evolution, versioning and backwards compatibility. You are able to add (i.e. append) new fields to an existing message without having to update all nodes, but if you attempt to remove a field or change the order of the fields appearing in a message, then all nodes of your distributed system will have to be updated with the new schema class code in order to use/support the new message format. There is also support for IDL, optional fields and repeating groups.