Chronicle favours modelling events as asynchronous method calls.
This document explains the events by method pattern used by Chronicle Wire.
An event is defined as a Java interface method which is written to a Wire by a
MethodWriter and dispatched to a handler by a MethodReader.
Advantages of this pattern include:
-
Direct method calls remain the simplest form of communication when the producer and consumer are in the same JVM.
-
Interfaces describing events become easy to serialise as data structures for persistence or testing.
-
Generated proxies minimise allocation and provide type safety.
- Event interface
-
A plain Java interface whose methods represent event types.
- Handler implementation
-
A class that implements the event interface to process events locally.
MethodWriter-
Generates a proxy that serialises method calls onto a
Wire. MethodReader-
Reads method calls from a
Wireand invokes them on a handler. - Wire format
-
YAML, JSON or binary encodings used to store or transmit the events.
sequenceDiagram
caller->>MethodWriter: invoke method
MethodWriter->>Wire: serialised call
MethodReader->>Handler: dispatch method
Typical use cases include:
-
High performance inter-process messaging.
-
Recording events to disk for replay during testing.
-
Converting asynchronous network traffic into method calls.
The simplest solution is to model a microservice as listening to one or more event sources and writing to a single event store. This is what the Chronicle Services framework does.
Chronicle Services added configuration of multiple microservices services, queues, redundancy, and restart policies.
Each event type has a different method to be called which implements code for that event type.
graph LR
A(Event Sources) --> B(Microservice<br/>Method Implementations)
B --> C(Event Store<br/>Chronicle Queue)
However, messages can still be produced and processed dynamically through the use of a wildcard event callback method where the event type is passed programmatically, for example void onEvent(String eventType, WireIn data).
A gateway handles information on behalf of remote clients. It has two main inputs: data from the client and data from the systems it talks to. In turn, it can produce two streams of output, one replies to the client and the other passes on data to internal systems.
graph LR
C(Client) --> G(Gateway)
S(Internal Systems<br/>sources) --> G
G --> CR(Client replies)
G --> DS(Internal Systems<br/>downstream events)
In both cases, these components can be tested using events stored in YAML files. Events appear as method names with their arguments, which may be simple scalars, a list of values or a DTO represented as a mapping.
# tiny illustration
myEvent: { someField: 1 }
anotherEvent: simpleValueThe same call written using TextWire would appear in a log or queue as:
myEvent:
someField: 1Depending on your use case, you can write YAML, JSON, Binary YAML (the BINARY or BINARY_LIGHT wire types), raw bytes or trivially copied objects as needed on a class by class basis.
It is common for most of the DTOs to have relatively low volumes and leaving them in the format easiest to work with is more maintainable, however the small number of high volume objects can be optimised as much as you need to meet your performance requirements.
Some advantages of YAML are:
-
Human-readable, more so than JSON.
-
More compact than JSON.
-
Supports type and comments.
-
Useful for configuration.
-
Interchangeable with other YAML libraries in other languages.
-
Capable of microsecond latencies for small messages.
Some advantages of JSON are:
-
Supported by browsers.
-
Somewhat human-readable.
-
Type support has been added in Chronicle Wire (
JSON_RETAIN_TYPE). -
Interchangeable with other JSON libraries in other languages.
-
Microsecond latencies are achievable.
Some advantages of Binary YAML are:
-
Natural arrangement for Java objects.
-
Support for schema changes, rearranging or removing fields and, with care, changing field types.
-
Compacts variable length data.
-
Supports type information.
-
Implicitly adds comments to help decode hex dumps.
-
Better performance than YAML (about half the latency).
-
Works provided the writer/reader have the Chronicle Wire library.
Some advantages of Trivially Copyable Objects
-
Raw memory copy for serialisation/deserialisation.
-
Compact memory structure provided fields don’t vary in size much.
-
Works provided the writer/reader arrange the fields the same way.
-
Better performance than Binary YAML (about half the latency).
Events received by a component can be handled in two main ways:
-
Raw Message Forwarding - the raw
BytesorDocumentContextcan be forwarded to another queue or network channel without deserialising the message fully. This is efficient for routers or filters that only inspect metadata. -
Full Deserialisation and Re-invocation - the event is deserialised and the corresponding method invoked locally; as part of the processing logic the same (or another) method can then be invoked downstream via a
MethodWriter.
The simplest use case is a single event with one argument.
interface Examples {
void noArgs();
void primArg(double value);
@MethodId(12) // use a compact method number instead of the method name
void withMethodId(long value);
void twoPrimArg(char ch, long value);
void scalarArg(TimeUnit timeUnit);
// encode a nanosecond resolution timestamp as a long, which appears as a timestamp in text formats, but an 8-byte long in memory
void timeNanos(@LongConversion(NanoTimestampLongConverter.class) long timeNanos);
void withDto(MyTypes dto);
}
static class MyTypes extends SelfDescribingMarshallable {
final StringBuilder text = new StringBuilder();
boolean flag;
byte b;
short s;
char ch;
int i;
float f;
double d;
long l;
}Chained events are useful for composing events. Routing, timestamp and metadata can be prepended to the event and removed as needed without altering the API of the underlying message.
In this example, routing information and a timestamp are added, but the underlying event isn’t altered to support this.
interface Saying {
void say(String hello);
}
interface Timed<T> {
T at(@LongConversion(NanoTimestampLongConverter.class) long time);
}
interface TimedSaying extends Timed<Saying> { }
interface Destination<T> {
T via(String via);
}
interface DestinationTimedSaying extends Destination<TimedSaying> { }A producer can use the fluent API as follows:
DestinationTimedSaying writer = wire.methodWriter(DestinationTimedSaying.class);
writer.via("targetQueue")
.at(System.nanoTime())
.say("Hello Composed World");A MethodReader recognises implementations that extends the marker interface @IgnoresEverything as one which doesn’t need to the called.
As such, if any stage in the chained call returns an implementation of this interface, the rest of the message is discarded and not read.
For convenience, there is a Mocker for generating such an implementation.
Saying that is recognised as ignoring everythingstatic final Saying SAY_NOTHING = Mocker.ignoring(Saying.class);
class MyTimedSaying implements TimedSaying {
public Saying at(long timeNS) {
if (isTooOld(timeNS))
return SAY_NOTHING;
return somethingElse;
}
}This pattern skips deserialisation of the remaining message when an early filter indicates the event should be ignored, saving processing time for irrelevant data.
An event type with String arguments
eg.say("Hello World");say: Hello World
...{"say":"Hello World"}11 00 00 00 # msg-length
b9 03 73 61 79 # say: (event)
eb 48 65 6c 6c 6f 20 57 6f 72 6c 64 # Hello WorldAny event type, single method calls only, with no arguments
eg.noArgs();noArgs: ""
...{"noArgs":""}09 00 00 00 # msg-length
b9 06 6e 6f 41 72 67 73 # noArgs: (event)
e0An event type with a single primitive arguments
eg.primArg(1.5);primArg: 1.5
...{"primArg":1.5}0c 00 00 00 # msg-length
b9 07 70 72 69 6d 41 72 67 # primArg: (event)
92 96 01 # 150/1e2An event type as a methodId with a single primitive arguments
eg.withMethodId(150);withMethodId: 150
...{"withMethodId":150}04 00 00 00 # msg-length
ba 0c # withMethodId
a1 96 # 150An event type with a two primitive arguments
eg.primArg('A', 128);twoPrimArg: [
A,
128
]
...{"twoPrimArg":[ "A",128 ]}15 00 00 00 # msg-length
b9 0a 74 77 6f 50 72 69 6d 41 72 67 # twoPrimArg: (event)
82 04 00 00 00 # sequence
e1 41 # A
a1 80 # 128An event type with a scalar arguments
eg.scalarArg(TimeUnit.DAYS);scalarArg: DAYS
...{"scalarArg":"DAYS"}10 00 00 00 # msg-length
b9 09 73 63 61 6c 61 72 41 72 67 # scalarArg: (event)
e4 44 41 59 53 # DAYSAn event type with a local date time as a long arguments
eg.timeNanos(NanoTimestampLongConverter.INSTANCE.parse("2022-04-29T08:24:17.44500531"));timeNanos: 2022-04-29T08:24:17.44500531
...{"timeNanos":"2022-04-29T08:24:17.44500531"}14 00 00 00 # msg-length
b9 09 74 69 6d 65 4e 61 6e 6f 73 # timeNanos: (event)
# 2022-04-29T08:24:17.44500531
a7 fe e7 cb 7c 70 50 ea 16 # 1651220657445005310An event type with a flat DTO
eg.withDto(new MyTypes().b((byte) -1).s((short) 1111).f(1.28f).i(66666).d(1.01).text("hello world").ch('$').flag(true));withDto: {
text: hello world,
flag: true,
b: -1,
s: 1111,
ch: $,
i: 66666,
f: 1.28,
d: 1.1234,
l: 0
}
...{"withDto":{"text":"hello world","flag":true,"b":-1,"s":1111,"ch":"$","i":66666,"f":1.28,"d":1.1234,"l":0}}45 00 00 00 # msg-length
b9 07 77 69 74 68 44 74 6f # withDto: (event)
80 3a # MyTypes
c4 74 65 78 74 # text:
eb 68 65 6c 6c 6f 20 77 6f 72 6c 64 # hello world
c4 66 6c 61 67 b1 # flag:
c1 62 # b:
a4 ff # -1
c1 73 # s:
a5 57 04 # 1111
c2 63 68 # ch:
e1 24 # $
c1 69 # i:
a6 6a 04 01 00 # 66666
c1 66 # f:
92 80 01 # 128/1e2
c1 64 # d:
94 e2 57 # 11234/1e4
c1 6c # l:
a1 00 # 0An event type can be chained together to compose routing or monitoring
eg.via("target").at(now).say("Hello World");via: target
at: 2022-04-29T08:24:17.46275735
say: Hello World
...{"via":"target","at":"2022-04-29T08:24:17.46275735","say":"Hello World"}2a 00 00 00 # msg-length
b9 03 76 69 61 # via: (event)
e6 74 61 72 67 65 74 # target
b9 02 61 74 # at: (event)
# 2022-04-29T08:24:17.46275735
a7 e6 c7 da 7d 70 50 ea 16 # 1651220657462757350
b9 03 73 61 79 # say: (event)
eb 48 65 6c 6c 6f 20 57 6f 72 6c 64 # Hello World