jonnifyechomq

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

GES GES GES GES GES GES read_window(conn, "cm", "round-feed", t0, t1) replay through a strategy the slice is the input — deterministic

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

mint time → t0 t1

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

the pattern (Redis Patterns Applied)

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.

the implementation (echo_mq)

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

Related in this course