Time-travel · dive 03
Backtest, audit, debug
One read answers three questions — how it behaved, what happened, what the state was — without keeping the window in memory.
The mint-instant window answers three different questions with the same call.
Each is a read_window/6 over a [t0, t1] interval — no
resident memory of the window required, because the store holds the log and the read returns only the slice.
Backtest replays a past slice through a strategy or a fold; audit reads exactly
what happened between two instants; debug folds the window up to a moment to reconstruct a past
state. Pick a use case on the right to see the one read each is. Worked on a codemojex round's
activity feed.
One window · three things to do with the slice
One read, three questions
The mint-instant window answers three questions with the same call. Each is a
read_window/6 over a [t0, t1] interval — no resident
memory of the window required, because the store holds the log and the read returns only the slice.
Backtest — how would this have behaved over that slice? Replay a past window of events through a strategy or a fold, deterministically, as many times as you like. The log is the input; the window selects the slice.
Audit — what happened between X and Y? Read exactly the entries in the interval,
in mint order, as the record of what occurred. The branded EVT id on each is the
canonical receipt — who, when, in what order.
Debug — what did the state look like at that moment? Fold the window up to an instant to reconstruct a past state, without having kept that state around. The log remembers; the read reconstructs.
None of the three needs a side table, a snapshot store, or a resident buffer of the window. The retained log plus a bounded read is the whole apparatus.
A worked example — replaying a codemojex round
Take the live consumer, codemojex (a Telegram emoji-guessing game on this stack). Each guess
is scored and recorded with its own branded GES id and a millisecond
timestamp: Codemojex.Guesses.submit/3 validates the guess,
Codemojex.Scoring.score/2 scores it against the secret, and the result is persisted
under a freshly minted GES id. Publish that same lifecycle moment to a retained stream
— a GES-branded EVT activity record per scored guess
— and a round's activity becomes a replayable log.
A round runs from when it opens to when Codemojex.Rooms.close_game/1 settles it (a
perfect score or an expired timer). To reconstruct that round's history — for an after-the-fact audit, a
scoring backtest, or a debug of a disputed result — you do not need to have streamed it live. You read the
window:
# A page-own codemojex example — replay one round's activity by its mint-time window. # The round's guess events were appended to the stream as they happened (the GES record # the live game already mints in Codemojex.Guesses.submit/3, scored by Scoring.score/2); # here we read the window [opened_at, settled_at] back, in mint order, after the fact. # the round's open and settle instants (settle is when Rooms.close_game/1 fired) {:ok, opened_at} = DateTime.from_iso8601("2024-05-01T14:30:00Z") |> ok() {:ok, settled_at} = DateTime.from_iso8601("2024-05-01T14:32:00Z") |> ok() # read exactly the round's window — a closed [t0, t1], both edges inclusive. # No resident memory of the window: the store held the log; the read returns the slice. {:ok, activity} = EchoMQ.Stream.read_window(conn, "cm", "round-feed", opened_at, settled_at) # fold the GES guess events into the round's history — in mint order, by construction. history = Enum.map(activity, fn {ges_id, fields} -> %{ id: ges_id, # the branded GES receipt — who/when, canonical player: fields["player"], emojis: fields["emojis"], points: fields["points"] # the Scoring.score/2 result, as recorded } end)
The window is the round; the slice is the round's guesses; the mint order is the order they were
played. Read the same window twice and you get the same history, folded the same way — the branded
GES id is both the receipt and the dedup key. That is the backtest property:
deterministic replay over a past slice.
The redis course builds the same activity feed from the pattern side — its Streams & Events workshop assembles a codemojex activity feed on the Stream Tier. This page reads it from the pillar-depth side: the mint-instant window over the retained log.
Replay a round · drag the open→settle window
The live log — and the door past it
read_window/6 reads the live log — the entries still resident on
the stream. That covers the recent past in full. Deep history — the entries a retention
policy has already trimmed off the live log — is not lost: it has folded to the durable floor. The
archive's merge-read serves archived ∪ live as one mint-ordered stream, so a window that reaches
before the live log still resolves.
That is the next module's surface — retention bounds the live log, and
EchoStore.StreamArchive folds what it trims into the durable
EchoStore.Graft floor. The mint-instant read here and the archive's merge-read there
are the same idea at two depths: time is the address, whether the entry is on the live tail or
on disk.
The door to persistence
Deep history beyond the live log folds to the durable floor —
EchoStore.StreamArchive into the EchoStore.Graft engine, on to
Tigris object storage — and the merge-read serves archived and live as one mint-ordered stream. The
durability dial in full is taught in Echo Persistence.
Pattern & implementation
Event sourcing replays a slice of the log to reconstruct state or test a change; an audit reads the range of what happened. Streams & Events teaches the replay, and its workshop builds the codemojex feed from the pattern side.
One read_window/6 over a [t0, t1] mint-instant interval
answers all three — backtest, audit, debug — deterministically and without resident memory, the
branded GES id the receipt and the dedup key. Deep history beyond the live log folds
to the archive.
The pattern names a replay; the implementation makes it a single bounded read over a log that remembers — the same call for how it behaved, what happened, and what the state was.
Recap — one read, three uses
Backtest, audit, and debug are one read — read_window/6 over a mint-instant
interval — applied three ways. The codemojex round example folds a window of GES
guess events into the round's history, deterministically, with the branded id as receipt and dedup key.
Time-travel covers the live log; deep history beyond it is the archive's merge-read, the door to
Echo Persistence. That completes the mint-instant read; retention and the
archive are next.
Next in the pillar: Retention & the archive — bound the live log, then fold what it trims to the durable floor, the door to Echo Persistence.
References
Where the three use cases are grounded, and the log they replay over.
Sources
- Valkey — XRANGE — the range read
read_window/6delegates to. - Valkey — Introduction to Streams — the retained log a window replays over.
- Kreps — The Log — the log as the source from which state is replayed.
- Lamport — Time, Clocks, and the Ordering of Events — the order a replay reads by.
Related in this course
- The two reads — the
read_window/6this dive applies. - The stream log — the append that put the
GESevents on the log. - Redis · Streams & Events — the pattern side, and the codemojex activity-feed workshop.
- Echo Persistence — the durable floor deep history folds into.
- Time-travel — the module hub.
- The Branded Component System · the Bus — the manuscript chapter this module realizes.