Operations
What a running Parsley application looks like from the cluster, what must exist before it starts, how it scales, what a reset discards, and how to size the two limits an operator owns. What to do when a process stops or holds is in Runbooks.
Names on the cluster
Every declared process becomes one Kafka Streams application with its own consumer group.
With ParsleyConfig.builder(bootstrap, "shop") and a process named shipper:
| What | Name |
|---|---|
| Application id and consumer group | shop-shipper |
| Ordering-state changelog | shop-shipper-__parsley.ordering-changelog |
Changelog of a declared store inventory |
shop-shipper-inventory-changelog |
| Transactional ids | derived by Kafka Streams from the application id |
The changelogs are created by Kafka Streams on first start, compacted. Every declared topic —
received and sent — must exist before Parsley.start; nothing is auto-created, and a missing
topic refuses the start.
What Parsley asks the cluster
Beyond the Streams application's own consumer and producer, each start uses the admin client
and up to two plain consumers: describing the declared topics by name, reading the ordering
changelog for prior state where one exists, listing the group's committed offsets, listing
the earliest or latest offset under read_committed — only for received partitions with
neither a committed position nor a covered position in the ordering state — and, where
committed positions are missing, joining the group as a short-lived bootstrap member to
commit the start positions under the group's generation fence. Each task initialisation
describes by id, once, the topics the task's state names, and corroborates by name any id
the broker does not know and whose name it has learned — a deletion or recreation is
confirmed only by three consistent answers half a second apart; an id whose name was never
learned is left alive. Nothing is asked of the broker between deliveries, and no consumer
beyond the Streams application's own is assigned to the partitions a held head waits on:
receipt of the named record is what settles a cause (D115). The ACLs those need are
Describe on every declared topic and on the group, Read on the group and on the received
topics, and whatever Streams itself needs to create and write its changelogs. A Describe
denial is treated as denial, never as a topic's deletion.
Scaling
Tasks are induced by the widest received topic: a process receiving a 12-partition topic runs as 12 tasks, spread over every instance started with the same application-id prefix. Adding instances rebalances tasks; each task's ordering state lives in its own changelog partition and follows it. Several instances may cold-start together: the runtime waits for another instance's bootstrap member to leave the group before joining, and a join that still meets one replaces its stream thread and joins again.
Widening the widest received topic changes the task width, which the ordering changelog
cannot follow, so it refuses at start with TASK_WIDTH_CHANGED; widening a narrower topic is
re-resolved on restart. Plan partition counts with the widest topic in mind.
Resetting a process
Several refusals end with "reset the process's state and group offsets deliberately". The reset discards the process's causal past: messages received after it may deliver before causes that were delivered before it, which is the boundary the refusal protects. The checks to make before it, the steps in the order that matters, and what to verify afterwards are in Runbooks.
Sizing
Metadata budget. Each emission carries the causal frontier: about 26 bytes for a topic's
first partition and 9 for each further partition, over the transitive upstream closure of
the process's inputs, external producers that stamp causes included. Set
metadataBudgetBytes comfortably above that sum; the default is 256 KiB, status() reports
each task's current width, and a warning is logged once at 80%.
Record limits. A held message is persisted to the ordering changelog with its payload,
its application headers and its causes, so the changelog's max.message.bytes and the
producer's max.request.size must cover the largest received record plus the metadata
budget plus a few hundred bytes of framing. The default 1 MiB record limit and the default
budget are compatible only for records well under 750 KiB; a held message beyond the limit
stops the process with SUBSTRATE_MISCONFIGURED naming the limit to raise.
Retention. A held message survives retention: it is persisted to the ordering changelog
and delivers from there, in order, once its causes settle, so retention need not cover
hold-back time. It must cover the longest stop and the longest lag on any received
partition: a committed read position below the log start — or, where the committed offset
has expired, the ordering state's covered position plus one — refuses at the fetch with
POSITIONS_DISCARDED_UNREAD (Failing closed). Add whatever a reset
that re-reads from EARLIEST should still find.
Reading the status
Parsley.status() reports each process's state, its refusal reason where it stopped
deliberately, and per task what is held and which cause each hold waits for, with the
frontier's size. A held message is not a failure: the diagnosis is the named cause. Every
refusal recurs identically on restart, except COVERED_POSITION_FED, which has no known
trigger and which a restart clears. What to do about each shape the status can show — a
refusal, a stop without one, a hold that does not move — is in Runbooks.