-
Notifications
You must be signed in to change notification settings - Fork 0
Wire Protocol
Evento v2 replaced v1's JSON-over-socket protocol with a binary CBOR protocol over Netty (4.2.x). Wire compatibility with v1 is intentionally broken — see Migrating from v1.
Every message on the TCP socket is:
[4 bytes length BE] ← LengthFieldPrepender / LengthFieldBasedFrameDecoder
[envelope header] ← CBOR-serialised Envelope record
[CBOR payload]
Identical on both sides of the connection:
EnvelopeFrameDecoder (LengthFieldBasedFrameDecoder, maxFrame 16 MB)
EnvelopeFrameEncoder (LengthFieldPrepender 4)
ChunkReassembler ← inbound: reassembles CHUNK frames by stream UUID
ChunkingEncoder ← outbound: FULL frame, or 1..N CHUNK frames
CborMessageDecoder ← produces Frame(message, rawBytes)
CborMessageEncoder ← encodes Message; passes ByteBuf through for sendRaw (zero-copy)
IdleStateHandler ← triggers heartbeat on write idle
HeartbeatHandler ← sends Ping; closes the channel on reader idle
BackpressureHandler ← transitions state → DEGRADED at the high-water mark
MessageInboundHandler ← dispatches Frame on the virtual-thread executor
The Netty EventLoop is never blocked: CBOR decoding and handler dispatch both hand off to the business executor.
ChunkingEncoder wraps each outbound ByteBuf as either:
- a
FULLframe —0x00+ CBOR data, when it fits in one chunk; or -
CHUNKframes —0x01+ 16-byte stream UUID + 1-byteisLastflag + data.
ChunkReassembler buffers chunks per stream UUID and only fires downstream on the last chunk.
maxFrameLengthbounds per-chunk memory, not message size. A message larger than the frame limit is transparently split. The server's test suite includes an 80 MB large-payload round trip.
In evento-transport-api, package com.evento.transport.message:
Message (sealed interface)
├── Hello (bundleId, instanceId, bundleVersion, authToken, capabilities)
├── Welcome (serverVersion, acceptedCapabilities)
├── Reject (reason — code constants: AUTH_FAILED, VERSION_MISMATCH, …)
├── Ping (seq, ts)
├── Pong (seq, ts)
├── Request (correlationId, payloadType: String, payload: byte[])
├── Response (correlationId, payload: byte[], failure: ResponseError)
└── Notification (payloadType: String, payload: byte[])
Adding a wire type is exactly two steps: extend the permits clause, and register a byte tag in
MessageTypeRegistry. The compiler then fails every non-exhaustive switch in the codebase until the
new type is handled — which is the point.
Note what Request / Response / Notification carry: a string payload type and an opaque byte
array. The server never deserializes the business payload; see design decision 1 in
Architecture Overview.
Constants in ProtocolNotifications, sent as Notification.payloadType:
| Payload type | Meaning |
|---|---|
evento:bundle-registration |
Lean registration — bundle version + handler payload types only |
evento:enable |
The bundle is ready to receive messages |
evento:disable |
The bundle should stop receiving messages |
evento:bundle-discovery |
Rich metadata (handlers + payloadInfo schemas), sent after enable |
Bundle Server
│── Hello(bundleId, instanceId, version, authToken) ──►│
│ TokenValidator.validate()
│◄── Welcome(serverVersion) ────────────────────────── │ (or Reject)
│── Notification(evento:bundle-registration, lean) ───►│ → BundleRegistered
│── Notification(evento:enable) ──────────────────────►│ → BundleEnabled
│── Notification(evento:bundle-discovery, rich) ──────►│ → BundleDiscovered
│ CommandBrokerHandler registers LocalRequestHandlers
Why registration and discovery are split. A bundle with thousands of handlers would exceed the
16 MB frame limit if all discovery metadata rode along with registration. So registration stays lean
(payload types only) and the rich metadata — handler schemas, payloadInfo, source locations —
follows post-enable as its own notification.
The broker relays Request / Response between bundles using Transport.sendRaw(byte[]) with the
raw bytes retained on the inbound Frame. No CBOR re-encoding on the broker hop.
This is a pinned contract, not an optimisation detail: BusLifecycle.forwardedRawCount and
forwardedReencodedCount are exposed as the meter evento.server.forwarded{path=raw|reencoded}, and
tests assert that every Netty-to-Netty forward takes the raw path.
Wire DTOs evolve by addition, and every codec tolerates unknown properties. All four mappers —
JacksonCborCodec, JacksonCborPayloadCodec, AdminPayloadCodec, ObjectMapperUtils — disable
FAIL_ON_UNKNOWN_PROPERTIES, so a peer one version ahead does not get its payload rejected. Records
normalise null to a default in their @JsonCreator, which covers the opposite skew.
CodecVersionToleranceTest pins both directions.
History — why this matters. The transport codecs used to be strict. A derived
isAsync()getter onRegisteredHandlertherefore made Jackson emit anasyncproperty that the broker rejected, which killed the entireBundleDiscoveryInfo. The failure was nearly invisible: the bundle registered, enabled and consumed normally while all of its handler metadata silently vanished from the dashboard, with onlyevent=listener_error … decode failed for BundleDiscoveryInfoin the log.Strictness was never the security control here. The deserialization defence is
MessageTypeRegistryand thePolymorphicTypeValidator, which bound which types may be instantiated; skipping an unrecognised property on an already-whitelisted type instantiates nothing. See Security Model.
When adding a field to a wire DTO: keep both properties in place, and give a new record component
a null-normalising @JsonCreator.
- Server Bus — what the broker does with these frames
- Bundle Client — the client side of the handshake
- Security Model — authentication, TLS, and the type whitelist
Evento Framework — Copyright 2020–2026 © Gabor Galazzo. Dual-licensed under AGPL-3.0 and a commercial licence.
This wiki documents the implementation; the repository is authoritative where the two disagree. Found something out of date? Open an issue.
Getting oriented
Internals
Operations
- Server Configuration
- Throughput and Capacity
- Observability
- Security Model
- Server REST API
- Troubleshooting
Project