Sticky routing and ordering
Two mechanisms with very different status:
ordering_required. SHIPPED. At most one consumer with outstanding leases. Strict serialised processing. Pulsar's "Exclusive" flavor. Does not scale beyond one consumer.- Sticky parallel routing. ROADMAP, not shipped for v1. Partition by
partition_key, same-key messages to the same consumer in a group, parallel across keys. The wire verb and the projection machinery exist but carry unfixed correctness holes (below). Do not build on it yet.
If you need ordering today, use ordering_required. Sticky parallel returns in the scale phase.
Sticky routing (roadmap)
Sticky parallel routing is parked by the CHARTER for v1. The AssignRange verb, the partition_key field, and the drain-on-reassignment fold are all present in the code, but three correctness holes are open and unfixed:
- Drain deadlock. A range's drain check never tests lease expiry, so a dead prior owner's expired lease wedges the range forever.
- Range-split bypass. Prior-owner detection is exact-bounds equality; splitting a range triggers no drain, so stale assignments accumulate.
- Consumer identity ignores the group. The ordering/drain identity compares the bare
consumer_idand ignoresconsumer_group_id.
These get fixed together when sticky returns. The design below is recorded so the intent is clear, not as a feature you can depend on.
The intended design:
Producers attach a partition_key: Option<u128> on each message. The projection hashes it with blake3 and takes the first 2 bytes little-endian. A u16 ring slot in 0..=0xFFFF.
Consumers register ranges with AssignRange { range_lo, range_hi, consumer_group_id, consumer_id }. The handler emits a RangeAssign control event. The fold appends the assignment.
plan_consume for a consumer in a group:
- Walks versions from
trim_cursortomessage_tail_version. - For each version with a partition_key, looks up the assigned consumer for that ring slot.
- Returns only versions whose ring slot is in a range owned by the asking
(group, consumer).
Versions with partition_key = None (the producer didn't set one) are eligible to any consumer in the group.
The draining protocol (intended)
When you reassign a range from consumer A to consumer B, A may still hold live leases on versions whose partition_key falls in that range. Without coordination, B could start consuming the same partition while A is still processing. Out-of-order delivery within a key.
The intended design is a mandatory drain. On RangeAssign to a new owner, the fold records pending_reassignments[range] = prior_owner. The new owner's plan_consume is blocked from versions in that range until A's last in-range lease drains (Ack, Nack, or expiry).
This is exactly where the open holes live: the drain check never tests lease expiry (deadlock on a dead prior owner), and a range split slips past the exact-bounds prior-owner check entirely. Until those are fixed, the drain does not hold under reassignment, which is why sticky parallel is parked.
ordering_required
Set ordering_required = true in QueueConfig. plan_consume enforces: if ANY other consumer has a live lease OR a pending Lease write in flight, return empty.
The "pending Lease write" check is load-bearing. Two consumers can race plan_consume on separate connections. If you only checked folded leases, both could pass the gate and both get assignments before either Lease event has committed. The projection tracks pending_lease[version] = consumer_id so the second consumer's plan_consume sees the first's reservation and bails. See INVARIANTS.md #23 and #37.
When ordering_required = true, dlq_strategy = Skip is rejected at queue creation. A skip would let a later message overtake the head-of-line position. The valid choices are Block or BlockAndDlq. On an ordering_required queue both HALT the line: plan_consume stops at the blocked head and delivers nothing past it until you send Unblock. Strict order preserved. Note this halt is specific to ordering_required — on a plain queue, Block only pins trim and skips the blocked version while later messages keep flowing. See DLQ and poison handling.
Choosing
What you can rely on today:
- No ordering requirement, want throughput. Plain queue, fan out across N consumers in the group, no
partition_key. Read DLQ and poison handling for Skip vs Block. - Strict serial processing (a command stream for a state machine).
ordering_required = true, one consumer at a time,dlq_strategy = BlockAndDlqso a poison message stops the line AND gets archived.
Partition-level ordering (per-key ordering, parallel across keys) is what sticky routing is for. It is parked for v1 — see the warning above. Until it ships, the only in-order option is ordering_required, which serialises the whole queue, not per key.