P07 — Stream-Processing System
Run it first. There is a companion page that builds this project's machinery as numbered, independently runnable blocks and then assembles them into one measured system: P07 hands-on — block by block (
handson/h*.py). Every number on it was produced by running the code. Read it alongside the milestones below.
Medium · 88 hours · Weeks 76–83 · Stage 3 · Go
Table of Contents
- The Loop, Instantiated
- Why This Project Matters
- Prerequisites
- Duration and Size
- Central Technical Questions
- Architecture
- Showcase — Do This Before You Start
- Implementation Milestones
- Concepts To Study
- Primary-Source Readings
- Experiments
- Benchmarks and Metrics
- Correctness Tests
- Failure Tests
- Expected Difficulties
- Scope Boundaries
- Deliverables
- Exit Criteria
- Extension Ideas
- Connections
- References
The Loop, Instantiated
| Step | For this project |
|---|---|
| 1. Problem | Compute continuously over an input that never ends, with events that arrive late, out of order, and sometimes twice |
| 2. Constraints | Unbounded input. Bounded memory. Events carry their own timestamps, which do not match arrival order. Failures happen mid-computation |
| 3. Naive design | Yours. People invent: a loop over a queue with a dict of counters; tumbling windows keyed on arrival time; "just batch every 5 minutes" |
| 4. Predicted failure | Your design has a correctness bug involving late events. Find it on paper. Then find the memory leak |
| 5. Minimal implementation | Partitioned log, offsets, one consumer, one stateful operator, processing-time windows |
| 6. Correctness | Output identical to a batch job over the same events — the reference oracle |
| 7. Instrumentation | Consumer lag, watermark position, state size, checkpoint duration, records dropped as late |
| 8. Baseline | A batch job over the same data. It is the ground truth |
| 9. Bottleneck | Is throughput bound by deserialisation, state access, checkpointing, or the downstream sink? |
| 10. Hypothesis | Checkpoint interval has an optimum trading steady-state overhead against recovery time. Predict it |
| 11. Modification | Incremental checkpointing |
| 12. Experiment | Interval sweep × failure rate × state size |
| 13. Failure analysis | Every duplicate or lost output traced to an interleaving |
| 14. Report | Why "exactly once" is a claim about effects, not about delivery |
Why This Project Matters
Batch processing has an easy definition of correct: the output is a function of the input, and the input is finite. Streaming has no such luxury. When the input never ends, you must decide what "the answer" means before you can compute it, and every streaming system is a set of answers to that question.
A watermark is not a feature. It is a formal admission that you are giving up on completeness in exchange for the ability to emit a result at all. Once you have built one, you can never again read "exactly-once semantics" on a marketing page without asking exactly-once with respect to what?
You already operate Kinesis and Flink-class systems. This project turns your operational knowledge into design knowledge, and it does so on the specific question that matters most in a news-recommendation context: how fresh can the system be, and what does freshness cost in correctness?
Prerequisites
- P05 complete — the replicated log becomes the event log; the fault injector is reused
- P06 helpful — the scheduler and worker pool generalise
- P04 helpful — operator state is an LSM in every real system
Duration and Size
Medium, 88 hours, 8 weeks.
| Tier | Contents | Hours |
|---|---|---|
| MVI | Partitioned append-only log with offsets, producer/consumer, consumer groups with rebalancing, one stateful operator, tumbling processing-time windows, at-least-once with offset commit. | 40 |
| Standard | + event time, watermarks, allowed lateness, sliding and session windows, checkpointing with recovery, exactly-once effects via idempotent/transactional sink, backpressure, incremental checkpoints. | 88 |
| Extension | Chandy–Lamport aligned barrier snapshots across a multi-operator DAG (the Flink design), with a measured comparison against the simple stop-the-world approach. | +35–50 |
Central Technical Questions
- What is the difference between event time and processing time, and what breaks if you use the wrong one? Give a concrete wrong answer your system would produce.
- What is a watermark, formally? It is an assertion — state it as one, including what happens when the assertion is false.
- Why is exactly-once delivery impossible, and what is achievable instead?
- What does a checkpoint have to capture for recovery to be correct? The answer includes something people forget: input positions.
- What is backpressure, and why is dropping data sometimes the correct response?
- How large can operator state get, and what happens when it exceeds memory?
Event time vs processing time — the concrete failure
A user reads an article at 23:58 on a phone that is offline. The phone syncs at 00:07. Your "articles read per day" job, keyed on processing time, attributes that read to the wrong day. Run it on a month of data and every daily number is wrong by the size of the offline-sync population — a systematic bias, not noise, and one that correlates with exactly the users you care about.
Keyed on event time, the read lands in the right day, but now the 23:00–00:00 window cannot be closed at 00:00, because more events may still arrive. You have traded a wrong answer for a late answer. That trade is the whole subject of this project, and the watermark is the dial.
Architecture
Write your naive design first.
producers ──► ┌──────────── log ─────────────┐
│ partition 0: [0][1][2][3]... │ append-only, offset-addressed
│ partition 1: [0][1][2]... │ retention by time or size
│ partition 2: [0][1][2][3]... │
└──────────────┬───────────────┘
│ consumer group: one partition → one consumer
▼
┌──────────────── operator ─────────────────┐
│ deserialize ─► assign event time │
│ ─► watermark tracker (min over partitions)│
│ ─► window assigner (tumbling/sliding/session)
│ ─► state store (keyed, LSM-backed) │
│ ─► trigger on watermark ─► emit │
│ ─► allowed lateness ─► late-firing / drop│
└───────────────┬───────────────────────────┘
▼
sink (idempotent by (window,key) OR transactional)
│
checkpoint: {operator state, input offsets, watermarks} ──► durable store
Watermarks, stated precisely
A watermark \(W(t)\) emitted at processing time \(t\) is the assertion:
No event with event time \(\le W(t)\) will arrive after this point.
Three consequences follow immediately, and they are the entire design space:
- The assertion can be wrong. An event arriving with event time below the current watermark is late. The system must have a policy: drop it, fire the window again with a correction, or route it to a side output. There is no fourth option and no option that is free.
- A watermark is a heuristic in any real system. Perfect watermarks require knowing the maximum possible delay, which you do not. Typical implementations use \(W(t) = \max(\text{observed event time}) - \delta\) for a chosen \(\delta\), and \(\delta\) is a completeness/latency dial with no correct setting.
- Watermarks must be the minimum across all inputs. One idle partition holds the watermark back forever and every window stalls. This is the single most common operational failure in streaming systems, and it has a standard fix (idle-partition detection with a timeout) that itself weakens the guarantee.
Your E3 measures the \(\delta\) frontier: completeness (fraction of events included in their correct window) against latency (time from window end to result emission).
Exactly-once, disassembled
The phrase means three different things and only two are achievable:
| Claim | Achievable? | Why |
|---|---|---|
| Exactly-once delivery | No | The two-generals problem. A sender cannot know whether a lost ack means the message arrived |
| Exactly-once processing | Yes, internally | Checkpoint state and input offsets atomically, and replay from the checkpoint |
| Exactly-once effect at the sink | Yes, with conditions | Requires an idempotent sink (keyed upsert) or a transactional one (two-phase commit with the checkpoint) |
The mechanism for the middle row: a checkpoint must contain operator state and the input offsets that produced it, written atomically. Recovering means restoring state and rewinding the input to the checkpointed offsets. If you checkpoint state and offsets separately, you get either duplicates or gaps depending on the order — which is a bug you should deliberately introduce and observe in E9.
This is the same idea as P06's atomic rename and P03's atomic segment flush. Three projects, one pattern: make the state transition and the position advance atomic. Say that in the report.
Showcase — Do This Before You Start
W4 · walkthroughs/w4_watermarks.py · ~45 minutes
A working miniature of this project: the same events counted three ways, and the completeness/latency frontier with its dead zone.
cd walkthroughs && python3 w4_watermarks.py
It is 80-ish lines and it surfaces this project's central surprise in an evening rather than in week six. Run it before committing the weeks.
Implementation Milestones
| # | Milestone | Hours | Done when |
|---|---|---|---|
| 1 | Partitioned append-only log: segments, offsets, retention | 8 | Reuses P04's segment machinery; offsets survive restart |
| 2 | Producer/consumer with offset commit; consumer groups + rebalancing | 8 | A consumer joining/leaving redistributes partitions with no loss |
| 3 | Operator runtime: source → operator → sink, with backpressure signalling | 8 | A slow sink slows the source rather than growing a queue |
| 4 | Keyed state store backed by P04's engine | 8 | State survives operator restart |
| 5 | Processing-time tumbling windows | 5 | Correct against a batch oracle for in-order data |
| 6 | Event time + watermark tracker (min across partitions, idle detection) | 10 | Watermark advances correctly with one idle partition |
| 7 | Sliding and session windows | 8 | Session gap semantics correct with out-of-order input |
| 8 | Allowed lateness: late firing, side output, drop policy | 6 | All three policies work and are measured |
| 9 | Checkpointing: state + offsets, atomically | 10 | Recovery produces batch-identical output |
| 10 | Exactly-once effects: idempotent sink and a transactional sink | 8 | Duplicate delivery causes no duplicate effect |
| 11 | Incremental checkpointing | 6 | Checkpoint duration decoupled from total state size |
| 12 | Experiments + report | 3 | All rows filled |
Concepts To Study
- Log abstraction: append-only, offset-addressed, retention; why a log is the right primitive for both messaging and state
- Partitions and keys: partitioning determines parallelism and ordering guarantees; ordering is per-partition only
- Consumer groups and rebalancing; the stop-the-world rebalance problem
- Event time, processing time, ingestion time — three clocks, all different
- Watermarks: the assertion, heuristic generation, the min-across-inputs rule, idle sources
- Window types: tumbling, sliding, session; and why session windows need merging
- Triggers and allowed lateness; the Dataflow model's separation of what, where, when, how
- State backends: in-memory vs LSM-backed; keyed vs operator state
- Checkpointing: stop-the-world vs Chandy–Lamport barriers; aligned vs unaligned
- Delivery semantics: at-most-once, at-least-once, exactly-once effects
- Backpressure: credit-based flow control vs blocking; why unbounded queues are the enemy
- Consumer lag as the primary operational metric
Primary-Source Readings
Budget: 12 hours.
| Reading | Why | Hours |
|---|---|---|
| Akidau, T. et al. The Dataflow Model. VLDB 2015 | The single most important paper here. The what/where/when/how decomposition | 3 |
| Akidau, T. Streaming 101 / 102. O'Reilly, 2015 | The clearest explanation of watermarks in print | 2 |
| Carbone, P. et al. Lightweight Asynchronous Snapshots for Distributed Dataflows. arXiv:1506.08603, 2015 | Flink's barrier snapshotting; the extension | 2 |
| Chandy, K. M., Lamport, L. Distributed Snapshots: Determining Global States of Distributed Systems. ACM TOCS 3(1), 1985 | The original algorithm underneath it | 1.5 |
| Kreps, J., Narkhede, N., Rao, J. Kafka: a Distributed Messaging System for Log Processing. NetDB 2011 | The log as a primitive | 1 |
| Kreps, J. The Log: What every software engineer should know about real-time data's unifying abstraction. 2013 | The conceptual essay; changes how you see storage generally | 1 |
| Zaharia, M. et al. Discretized Streams. SOSP 2013 | The micro-batch alternative, and its honest trade-offs | 1.5 |
Experiments
| # | Experiment | Sweep | Predict first |
|---|---|---|---|
| E1 | Event time vs processing time | on data with a realistic delay distribution | Quantify the daily-attribution error from the example above |
| E2 | Out-of-order severity | delay distribution: none / exponential / heavy-tailed | Completeness vs watermark delay |
| E3 | Watermark delay δ | {0, 1 s, 10 s, 60 s, 5 min} | The completeness/latency frontier. Predict both endpoints |
| E4 | Allowed lateness policy | drop / late-fire / side-output | Correctness and output volume |
| E5 | Checkpoint interval | {1 s, 10 s, 60 s, 300 s} × failure rate | Steady-state overhead vs recovery time: predict the optimum |
| E6 | Incremental vs full checkpoint | state ∈ {10 MB, 1 GB, 10 GB} | Where does incremental start to matter? |
| E7 | Recovery time | vs state size and vs checkpoint age | Predict linear in both; check the constants |
| E8 | Consumer lag under burst | 10× input spike for 60 s | Recovery time to zero lag; predict from throughput headroom |
| E9 | Duplicate delivery | at-least-once vs exactly-once sink | Count duplicate effects; must be zero for the latter |
| E10 | Backpressure | sink slowed 10× | Lag grows, memory does not. Verify the second part |
| E11 | Key skew | Zipfian keys | Per-partition lag imbalance |
| E12 | State size growth | session windows with no timeout | It grows without bound. Measure the leak, then fix it |
| E13 | Slow downstream consumer | one consumer in a group at 10× latency | Does it stall the group? |
E3 is the project's headline. Plot completeness (fraction of events counted in their true window) against emission latency, one point per δ. That curve is the streaming version of P02's recall/QPS curve, and it makes the same point: you choose an operating point on a frontier; there is no correct answer, only a stated one.
E12 is the trap worth falling into on purpose. Session windows with no timeout accumulate state for every key ever seen. Watch memory grow, then implement state TTL and watch it stop. Unbounded state is the production failure of streaming systems.
Benchmarks and Metrics
| Metric | Notes |
|---|---|
| Throughput (events/s) | Sustained, not peak; state the state size |
| End-to-end latency p50/p95/p99 | Event time → result emitted. p99 is what SLOs are written against |
| Consumer lag | Records and seconds, per partition. The primary operational metric |
| Watermark lag | Wall clock minus watermark; distinct from consumer lag and often more informative |
| Completeness | Fraction of events included in their correct window |
| Late-event rate | By how late, as a distribution |
| Checkpoint duration and size | Full and incremental |
| Checkpoint overhead | % throughput lost to checkpointing |
| Recovery time | Failure to caught-up |
| State size | Per operator, over time — plot it; leaks are visible as slope |
| Duplicate effect count | Must be zero for exactly-once |
| Memory under backpressure | Must be bounded. Plot it |
Correctness Tests
- Batch equivalence. For any finite prefix of the stream, streaming output with a sufficiently large watermark delay equals the batch job's output. This is the oracle; everything else is a special case of it.
- Idempotent recovery: kill and restart at 20 random points; final output unchanged.
- No lost events at any watermark setting — every event either lands in a window, fires late, or is explicitly counted as dropped. The three must sum to the input.
- Offset/state atomicity: after recovery, the state matches exactly the events before the committed offset.
- Window boundary correctness: an event exactly on a boundary lands in exactly one window. Test both boundaries.
- Session merge: overlapping sessions merge correctly when a late event bridges them — the hardest window semantics to get right.
- Watermark monotonicity: the watermark never goes backwards.
- Idle-partition handling: watermark advances when one partition is silent.
- Rebalance safety: no event processed twice, none skipped, across a group rebalance.
- Bounded memory under sustained backpressure.
Failure Tests
| Injection | Required behaviour |
|---|---|
| Kill an operator mid-window | Recovers from checkpoint; output matches batch |
| Kill during a checkpoint | Old checkpoint still valid; no corruption |
| Duplicate every event | Exactly-once sink produces no duplicate effect |
| Reorder within a partition | Event time handles it up to δ; beyond δ they are late |
| Events 1 hour late | Policy applied and counted, not silently dropped |
| One partition idle for 10 minutes | Watermark still advances (with idle detection) |
| Sink unavailable for 60 s | Backpressure; bounded memory; no data loss |
| Consumer joins mid-stream | Rebalance without loss or duplication |
| Clock skew across nodes | Event-time results unaffected |
| State store disk full | Clean failure, recoverable |
| 10× input burst | Lag grows and recovers; no OOM |
Expected Difficulties
- Session windows are the hardest semantics in the project. A late event can bridge two existing sessions, requiring a merge and a retraction of previously emitted results. Budget real time for milestone 7.
- Watermark propagation across operators is subtle: each operator's output watermark is a function of its input watermarks and its own buffering. Get it wrong and windows fire early — silently.
- "Exactly once" will tempt you into over-claiming. Be precise in the report about which of the three claims you implemented and under what sink assumptions.
- Testing streaming is harder than testing batch because time is an input. Make the clock injectable from milestone 1 — every test drives time explicitly. Retrofitting this is a rewrite.
- State growth is silent until it is fatal. Plot state size in every experiment from the start.
- Backpressure that "works" by buffering is not backpressure. Test bounded memory explicitly (E10), not throughput.
Scope Boundaries
In scope: single-node or few-process, a partitioned log, one operator DAG of modest depth, event time and watermarks, three window types, checkpointing, backpressure, exactly-once effects.
Out of scope: a distributed scheduler with dynamic rescaling; SQL over streams; a query optimiser; multi-DAG multi-tenancy; a replicated log with consensus (P05 already did that — reuse or simulate); machine learning on streams; a web UI.
Deliverables
streamproc/— Go, with the log, runtime, windowing, and checkpointingREPORT.mdcentred on the E3 completeness/latency frontier- The batch-equivalence test harness — reusable and genuinely valuable
- Notebook entries for E3, E5, E9, E12
- A state-size-over-time plot for every experiment (the leak detector)
Exit Criteria
- Batch equivalence holds for all window types at sufficient watermark delay
- Recovery from ≥20 random kill points produces batch-identical output
- E3 complete: the completeness/latency frontier plotted across five δ values
- E5 complete: checkpoint-interval optimum identified and explained
- E9 complete: exactly-once effects verified with zero duplicates under duplicate delivery
- E10 complete: memory bounded under sustained backpressure, plotted
- E12 complete: unbounded state observed, then fixed with TTL, both measured
- Late-event policy implemented in all three variants and counted
-
REPORT.mdwritten with a falsified prediction
Extension Ideas
- Chandy–Lamport barrier snapshots across a multi-operator DAG, compared against stop-the-world. Measure the throughput impact of alignment — and of unaligned checkpoints under backpressure, which is the modern Flink answer.
- Retractions: emit corrections when late data changes a previously emitted result, and handle the downstream consequences.
- Watermark-delay auto-tuning from the observed lateness distribution. A research direction.
- Streaming joins with two watermarks and state expiry on both sides.
Connections
Backward: P05's replicated log becomes the event log; the fault injector is reused. P04 backs the state store. P06 supplies the worker/scheduler patterns.
Forward:
- → P08/P09: real-time interaction ingestion; the EMA user profile is a stateful streaming operator, and computing it here rather than in a batch job changes the freshness/complexity trade materially
- → P15: the ingestion layer. The research question "how do storage and indexing choices affect recommendation freshness?" is answered largely here
References
- Akidau, T. et al. The Dataflow Model: A Practical Approach to Balancing Correctness, Latency, and Cost in Massive-Scale, Unbounded, Out-of-Order Data Processing. VLDB 8(12), 2015.
- Akidau, T. Streaming 101: The world beyond batch and Streaming 102. O'Reilly Radar, 2015.
- Carbone, P., Fóra, G., Ewen, S., Haridi, S., Tzoumas, K. Lightweight Asynchronous Snapshots for Distributed Dataflows. arXiv:1506.08603, 2015.
- Chandy, K. M., Lamport, L. Distributed Snapshots: Determining Global States of Distributed Systems. ACM TOCS 3(1), 1985.
- Kreps, J., Narkhede, N., Rao, J. Kafka: a Distributed Messaging System for Log Processing. NetDB 2011.
- Kreps, J. The Log: What every software engineer should know about real-time data's unifying abstraction. LinkedIn Engineering, 2013.
- Zaharia, M., Das, T., Li, H., Hunter, T., Shenker, S., Stoica, I. Discretized Streams: Fault-Tolerant Streaming Computation at Scale. SOSP 2013.
- Carbone, P. et al. Apache Flink: Stream and Batch Processing in a Single Engine. IEEE Data Engineering Bulletin 38(4), 2015.
- Abadi, D. J. et al. The Design of the Borealis Stream Processing Engine. CIDR 2005.
- Kleppmann, M. Designing Data-Intensive Applications, ch. 11. O'Reilly, 2017.