Wire format
Byte layouts for everything Parsley writes: the one record header, and the state-store entries. All integers are big-endian. Layouts are versioned where they travel between processes; pre-1.0, versions change without compatibility aliases.
The clock
The serialized form of the internal vector clock, used both in the record header and inside state values. Both sections are sorted, so equal clocks are byte-identical.
byte version (currently 1)
int32 offsetEntryCount
entry × offsetEntryCount:
int64 topicId, most significant bits
int64 topicId, least significant bits
int32 partition
int64 offsetWatermark
int32 seqEntryCount
entry × seqEntryCount:
int64 topicId, most significant bits
int64 topicId, least significant bits
int32 partition
int64 senderId, most significant bits
int64 senderId, least significant bits
int64 seqWatermark
An offset entry claims every offset at or below the watermark on the channel. A sequence entry claims every record the sender sent to the channel with per-channel send sequence at or below the watermark — the sender-synchronous claim kind that keeps the stamping path non-blocking (see the stamp).
A malformed clock — unknown version, negative fields, length mismatch — throws
CorruptClockException. A present but undecodable clock always fails the task; it never reads
as empty (fail closed).
Record headers
| Header | Value |
|---|---|
vc |
The record's dependency clock, serialized as above. Absent means the producer claims nothing. |
vc-sender |
The producing node's stable sender identity: 16 bytes, UUID most- then least-significant. Absent on edge-produced records. |
vc-seq |
The record's per-channel send sequence at its sender: 8 bytes. Present exactly when vc-sender is. |
These headers, plus the state stores below, are Parsley's entire byte footprint.
State-store keys
Keys are UTF-8 strings; <channel> is <topicId-uuid>:<partition>. Values:
| Key | Value layout |
|---|---|
scope |
int32 channelCount, then per channel int64 msb, int64 lsb, int32 partition; int32 sinkCount, then per sink int64 msb, int64 lsb |
f/<channel> |
int64 frontier offset |
cc/<channel> |
A serialized clock (the channel's advertised view) |
ca |
A serialized clock (carried ancestry) |
q/<channel>/m |
int64 headIndex, int64 tailIndex (one past the last held entry) |
q/<channel>/e/<index> |
A held record (below); <index> is 16 lower-case hex digits, zero-padded so lexicographic order is numeric order |
sq/<channel> |
int64 — the last send sequence this node issued to the sink channel |
ds/<channel>/<senderId> |
int64 seq, int64 offset — the highest delivered sequence from that sender on the channel, and its offset (sequence-claim resolution state) |
Held record
A record held in a channel's queue persists verbatim, so a restart restores exactly what was fetched:
int64 offset
int64 timestamp
byte tagged (0 = untagged sender)
int64 senderId, most significant bits (zero when untagged)
int64 senderId, least significant bits (zero when untagged)
int64 senderSeq (meaningful only when tagged)
int32 clockLength
bytes clock (serialized clock; empty clock when the header was absent)
int32 keyLength (-1 = null key)
bytes key
int32 valueLength (-1 = null value)
bytes value
Non-Parsley headers are not yet part of the envelope; a held record is delivered with empty headers. This is a known limit of the current cut.
Tick record
A stage with declared ticks emits one record per interval per task to its tick topic,
<application.id>-<stage>-ticks. The key is the emitting task's partition as a big-endian
int32 — the sink partitioner reads it back to land the tick on the emitting task's own
channel — the value is null, the timestamp is the task's wall clock at emission, and the headers are the
standard stamp above.
Fold-state store
A stateful stage keeps its per-key fold state in a second store, <stage>-fold — which
Kafka Streams backs with the changelog topic <application.id>-<stage>-fold-changelog.
Keys are the message key's encoded bytes with a one-byte presence prefix — 0x00 alone for
a null key, 0x01 followed by the key bytes otherwise — so a null key folds under its own
state without colliding with an empty key. The tick state occupies the single key 0x02,
its own namespace beside the presence prefixes. Values are whatever the stage's state codec
produces; Parsley adds no framing.
Not persisted
ownOutputs (re-seeded from sink end offsets at every init, which dominate any persisted
copy) and the position-known watermark (re-reported by the host's position advances). Their
absence from the store is deliberate; see state and recovery.