Replay determinism
Break it on purposeNeeds Python
Problem
Exactly-once processing in stream engines is replay from a checkpoint: after a crash, restore the state saved at the last checkpoint and reprocess the events after it. The final state equals a crash-free run only if reprocessing an event produces exactly what the first processing did. A computation that reads a clock or a random number produces something different on replay, and nothing in the engine detects it. On the output side, writes repeated by the replay are harmless only if the sink is idempotent, for example an upsert keyed by event id.
Write a tiny exactly-once pipeline: a loop that reads a list of events, updates a dictionary, and "checkpoints" by saving the dictionary and the position every 500 events. Crash it at a random position and restart from the checkpoint, 50 times. First make the computation pure. Then add random.random() to it and repeat. Then make the output an upsert keyed by event ID.
The question. Does the final state match a crash-free run after each crash, and what do replayed writes do to an upsert sink?
Predict
The pure version gives the same final state every time. The impure version gives final states that differ from a crash-free run, and nothing in your program reports the difference. With an upsert keyed by event ID, duplicates (replayed events written twice) are harmless.
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide the code
replay-determinism.py # /// script # requires-python = ">=3.10" # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Replay determinism. A tiny exactly-once pipeline: a loop over 10,000 events updates a dictionary and checkpoints (state + position) every 500 events. It is crashed at a random position and restarted from the checkpoint, 50 times, first with a pure computation and then with one that reads random.random(). Each final state is compared with a crash-free run. Then the impure output is written as an upsert keyed by event id, and replayed events are counted. """ import argparse, copy, json, random p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--events", type=int, default=10_000, help="Events.") p.add_argument("--every", type=int, default=500, help="Checkpoint interval.") p.add_argument("--trials", type=int, default=50, help="Crash-and-restart trials.") p.add_argument("--seed", type=int, default=0, help="Random seed.") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() rng = random.Random(args.seed) events = [{"id": i, "user": f"u{rng.randrange(50)}", "amount": rng.randrange(1, 100)} for i in range(args.events)] def step(state, e, impure): bonus = random.random() if impure else 0.0 # the hidden non-determinism state[e["user"]] = state.get(e["user"], 0) + e["amount"] + bonus def run(impure, crash_at=None, sink=None): ckpt = ({}, 0); state, pos = {}, 0; replayed = 0 while pos < len(events): e = events[pos]; step(state, e, impure) if sink is not None: if e["id"] in sink: replayed += 1 sink[e["id"]] = round(state[e["user"]], 6) # upsert keyed by event id pos += 1 if pos % args.every == 0: ckpt = (copy.deepcopy(state), pos) if crash_at is not None and pos == crash_at: state, pos = copy.deepcopy(ckpt[0]), ckpt[1]; crash_at = None # crash: restart from the checkpoint return state, replayed rows = [] for impure in (False, True): random.seed(1); base, _ = run(impure) differ = 0 for t in range(args.trials): crash = rng.randrange(1, args.events); random.seed(1) # same seed as the crash-free run: only the replay differs st, _ = run(impure, crash) if any(abs(st.get(k, 0) - v) > 1e-9 for k, v in base.items()): differ += 1 rows.append({"computation": "random.random() inside" if impure else "pure", "trials": args.trials, "final_state_differs": differ}) sink = {}; random.seed(7); _, replayed = run(True, crash_at=args.events // 2 + 123, sink=sink) summary = {"sink_rows": len(sink), "events": args.events, "replayed_writes": replayed} for r in rows: print(f"{r['computation']:<24} final state differs from the crash-free run in {r['final_state_differs']} of {r['trials']} crashes (no error raised)") print(f"upsert keyed by event id: {replayed} replayed writes, {len(sink):,} rows for {args.events:,} events") if args.json: with open(args.json, "w") as f: json.dump({"summary": summary, "tables": {"computations": rows}}, f)3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.
Terminal uv run replay-determinism.py uv run replay-determinism.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --events10,000 | Events. |
| --every500 | Checkpoint interval. |
| --trials50 | Crash-and-restart trials. |
| --seed0 | Random seed. |
Our run
50 crashes and restarts
| Computation | Final state differs | Of |
|---|---|---|
| pure | 0 | 50 |
| random.random() inside | 50 | 50 |
pure final state differs from the crash-free run in 0 of 50 crashes (no error raised)
random.random() inside final state differs from the crash-free run in 50 of 50 crashes (no error raised)
upsert keyed by event id: 123 replayed writes, 10,000 rows for 10,000 eventsWhat we found
The pure computation matched the crash-free run after every crash (0 of 50 differed). With random.random() inside, 50 of 50 runs ended in a different state, and nothing raised an error. On the output side, the replay wrote 123 events a second time, and the upsert sink still held exactly 10K rows for 10K events.
Read it
Exactly-once is a property of the computation as much as of the engine: the engine guarantees each event's effect is applied once, from state that was correct, and your code must make that effect the same every time. Push non-determinism into the input (an event's own timestamp, a seed stored with the event) and make sinks idempotent.