pa-04 — The Hitchhiker's Guide to the Partitioned Log
Companion to CONCEPTS.md, with the runnable log in
src/go/plog/. pa-03 gave you the event patterns; this is the substrate — the one data structure under Kafka, Pulsar, and NATS JetStream.
bash scripts/verify.sh runs the demo: all acct-1 records share a
partition (ordered), a consumer group commits and resumes, partitions get
assigned across consumers two ways, and retention truncates while offsets
stay absolute.
1. The log + partitioning (log.go)
A PartitionedLog is n append-only slices. Produce(key, value) hashes
the key (PartitionFor) to pick a partition, appends, and returns a
monotonic offset. The two facts that flow from this:
- Same key → same partition → ordered.
TestKeyMapsToSamePartitionandTestPerPartitionOrderingprove allacct-1records land together with offsets0,1,2,…in order. Ordering is only within a partition — the single most important property to internalize. - Offsets are absolute and stable.
Consume(partition, fromOffset, max)returns records at/after an offset;HighWatermarkis the next offset. A consumer polls from where it left off — reading doesn't consume (unlike a queue), so many consumers and replay coexist.
The architecture decision hiding here is the partition key. Key by
accountId and you get per-account ordering but a hot account is a hot
partition (skew); key randomly and you get even load but no ordering.
That trade-off is yours to own.
2. Consumer groups + offset resume (groups.go)
GroupOffsets tracks per-(group, partition) progress. TestConsumerGroupOffsets
walks the lifecycle: a fresh group starts at 0, reads 2, commits next=2,
"restarts," and resumes at offset 2 — and a different group reads the
same log independently from 0. That independence is the log's superpower:
billing and analytics consume the same orders stream at their own pace,
and a new consumer can replay history.
Commit timing is a delivery-semantics choice: commit after processing for at-least-once (a crash re-delivers — needs idempotency, pa-03); commit before for at-most-once (a crash loses it). The architect picks per consumer.
3. Assignment & rebalancing (groups.go)
A consumer group splits a topic's partitions across its members.
AssignRange (contiguous chunks; earlier consumers get the extra) and
AssignRoundRobin (one at a time) are Kafka's classic strategies —
TestRangeAssignment/TestRoundRobinAssignment pin both for 5 partitions
over 2 consumers. The ceiling: parallelism caps at the partition
count — a 5th consumer on a 4-partition topic sits idle. Rebalancing on
membership change is disruptive (stop-the-world reassign); production adds
sticky/cooperative rebalancing to minimize movement — the exact same
stability goal as gw-04's subsetting ring and pa-06's consistent hashing.
4. Retention (log.go)
Truncate(partition, beforeOffset) drops old records, but survivors keep
their original absolute offsets (TestRetentionKeepsOffsetsStable:
after truncating below 3, the first record is still offset 3, and the high
watermark is unchanged). So committed offsets stay valid; a consumer that
fell behind the retention window simply resumes at the earliest retained
record — having silently lost the truncated data. That's the operational
risk: retention is a moving cliff behind your slowest consumer, so you
monitor consumer lag (HighWatermark − committed) as an SLI (pa-09) and
alert before the cliff.
5. Why this is the architect's keystone for eventing
Once the log is concrete, the whole eventing stack composes:
- pa-03's pub/sub, at-least-once, and DLQ run on this log.
- pa-05's outbox publishes to it; idempotent consumers handle its at-least-once redelivery.
- Event sourcing is "the log is the source of truth; state is a fold over it" — replay from offset 0.
- Choosing Kafka vs Pulsar vs NATS vs RabbitMQ becomes a concrete comparison of this model (log + groups + retention) vs a queue model.
6. Hands-on
cd src/go
bash ../scripts/verify.sh
go run ./cmd/plogsim
7. Exercises
- Consumer lag SLI: compute
HighWatermark − committedper partition and alert when it exceeds a threshold (wire to pa-09). - Hot-partition detection: produce skewed keys and measure per-partition record counts; show how a bad key creates skew.
- Sticky rebalancing: implement an assignor that, when a consumer leaves, reassigns only its partitions and keeps others put; compare movement to range/round-robin (the gw-04/pa-06 stability theme).
- Replay: reprocess a partition from offset 0 with a new consumer group; show idempotent consumers (pa-03) make this safe.
- Time-based retention: add timestamps and truncate by age; reason about the lag/retention SLO relationship.