Skip to content
About
Main content

Replay a durable log

Break it on purposeNeeds Python

Problem

survives crashes by recording each step's result in a log and, on restart, replaying the code from the beginning while answering completed steps from the log instead of running them again. That works only if the workflow code is deterministic: on it must call the same steps in the same order. Reading the clock or a random number in the workflow itself breaks the match between the code and its log.

Write a toy durable-execution runtime: an activity(fn, *args) function that checks a JSON file for a recorded result for step number k, and runs and records fn only if there is none. Write a workflow with three activities and kill the process (os._exit(1)) after the second. Restart it. Then change the workflow to call time.time() and branch on it, and replay it.

The question. Does the restart skip the completed activities, and what happens when the workflow branches on the clock?


Predict

On replay, a workflow that branched on time.time() reaches the other branch. What happens?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    replay-a-durable-log.py69 linesDownload
    View the code
    replay-a-durable-log.py
    # /// script
    # requires-python = ">=3.10"
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """Replay a durable log.
    
    A toy durable-execution runtime: activity(fn, *args) returns the recorded
    result for step k from a JSON log if there is one, and otherwise runs fn and
    records it. A workflow of three activities is killed (os._exit) after the
    second; the second start replays 1 and 2 from the log and runs only 3. Then
    a workflow that branches on time.time() is replayed and diverges.
    """
    import argparse, json, os, subprocess, sys, tempfile, time
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--json", help="Also write the results to this JSON file.")
    args = p.parse_args()
    
    RUNTIME = r'''
    import json, os, sys, time
    LOG = sys.argv[1]; MODE = sys.argv[2]; CRASH = sys.argv[3] == "crash"
    log = json.load(open(LOG)) if os.path.exists(LOG) else []
    step = [0]
    def activity(fn, *a):
        k = step[0]; step[0] += 1
        if k < len(log):
            if log[k]["name"] != fn.__name__:
                print(f"DIVERGED at step {k}: log has {log[k]['name']}, workflow called {fn.__name__}"); sys.exit(2)
            print(f"replayed {fn.__name__} from the log"); return log[k]["result"]
        r = fn(*a); log.append({"name": fn.__name__, "result": r})
        json.dump(log, open(LOG, "w")); return r
    def charge(x): print("RAN charge"); return x * 2
    def ship(x): print("RAN ship"); return x + 1
    def email(x): print("RAN email"); return x
    def refund(x): print("RAN refund"); return -x
    a = activity(charge, 21)
    if MODE == "clock" and int(time.time() * 10) % 2: b = activity(refund, a)       # branches on the clock: not deterministic
    else: b = activity(ship, a)
    if CRASH: os._exit(1)                                                          # killed after the second activity
    c = activity(email, b); print("done", c)
    '''
    d = tempfile.mkdtemp(); src = os.path.join(d, "wf.py"); open(src, "w").write(RUNTIME)
    def start(log, mode, crash):
        r = subprocess.run([sys.executable, "-u", src, log, mode, "crash" if crash else "ok"], capture_output=True, text=True)
        return r.returncode, [l for l in r.stdout.splitlines()]
    
    rows = []
    log = os.path.join(d, "det.json")
    for i, crash in enumerate((True, False)):
        code, out = start(log, "det", crash)
        rows.append({"workflow": "deterministic", "start": i + 1, "exit": code, "ran": ", ".join(l[4:] for l in out if l.startswith("RAN")) or "-",
                     "replayed": ", ".join(l.split()[1] for l in out if l.startswith("replayed")) or "-", "outcome": "killed" if code == 1 else out[-1]})
    diverged = 0
    for attempt in range(10):                                              # try until the clock picks the other branch on replay
        log = os.path.join(d, f"clock{attempt}.json")
        code1, out1 = start(log, "clock", True); time.sleep(0.05); code2, out2 = start(log, "clock", False)
        if code2 == 2:
            diverged += 1
            rows.append({"workflow": "branches on time.time()", "start": 2, "exit": code2, "ran": ", ".join(l[4:] for l in out2 if l.startswith("RAN")) or "-",
                         "replayed": ", ".join(l.split()[1] for l in out2 if l.startswith("replayed")) or "-", "outcome": out2[-1]})
            break
    summary = {"clock_attempts": attempt + 1, "diverged": "yes" if diverged else "no"}
    for r in rows: print(f"{r['workflow']:<24} start {r['start']}: ran [{r['ran']}], replayed [{r['replayed']}] -> {r['outcome']}")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"summary": summary, "tables": {"starts": rows}}, f)
    
  3. 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-a-durable-log.py
    uv run replay-a-durable-log.py --help     # every knob

Our run

Starts of the workflow

Starts of the workflow
WorkflowStartRanReplayed from the logOutcome
deterministic1charge, ship-killed
deterministic2emailcharge, shipdone 43
branches on time.time()2-chargeDIVERGED at step 1: log has ship, workflow called refund
Reference runApple M2 · 8 cores · 8 GB
Output · uv run replay-a-durable-log.py
deterministic            start 1: ran [charge, ship], replayed [-] -> killed
deterministic            start 2: ran [email], replayed [charge, ship] -> done 43
branches on time.time()  start 2: ran [-], replayed [charge] -> DIVERGED at step 1: log has ship, workflow called refund
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

What we found

The first start ran charge, ship and was killed. The second replayed charge, ship from the log without running them and ran only email. The workflow that branched on the clock took the other branch on replay and stopped: DIVERGED at step 1: log has ship, workflow called refund.


Read it

That is the determinism constraint, measured. Anything non-deterministic (time, randomness, reading a mutable store) must happen inside an activity, whose result is recorded, never in the workflow code that is replayed. Real runtimes detect the divergence as this toy does; the fix is always to move the read into a step.