Replay
A recorded session re-folded through the real single-writer core from its first checkpoint and fenced against the recorded final state hash — what replays, what is deliberately replay-neutral, the three fences, and the crash-restart restore that shares the engine.
crates/vike-core/src/replay.rs is the payoff of the journal: a recorded session re-derived
through the same runtime it was recorded on, and proven bit-identical. Its header says what that
buys twice over — it gates the journal format, because any nondeterminism in the fold surfaces as
a mismatch, and it is the substrate the crash restart restores from. Two entry points share one
engine, replay_from. replay_offline restores from the first Snap, re-folds the whole
tail, and fences the reproduced final hash against the source journal's last Snap.
restore_from_journal restores from the latest Snap, re-folds only the crash tail after
it — empty on a clean shutdown — and applies no fence, because it is a restore.
The mechanism
Restoring from the first checkpoint is what makes the fence worth anything: restoring from the
latest would leave nothing to replay on a clean run, since the exit Snap already reflects
everything. Cadence Snaps are taken after the fold, so every command before the first one is
already folded into it and every command after it is re-folded forward. From that engine
replay_from spawns a real core with three substitutions. ReplayClient is a pure no-op
ExecutionClient: it suppresses every venue side effect and synthesizes no events — the original
venue events are already in the journal as Ingest::Event records and are re-pumped, so a client
that also emitted them would double the state and correctly break the fence. QueueClock replays
the tail's recorded now_ms stamps in order, so timestamp-derived state such as an order's
created_ms matches. The temp journal is opened with snapshot_every = u64::MAX, so only the
exit Snap fires and carries the reproduced hash. The tail is pumped through the handle in journal
order — events and commands feed one FIFO queue, so dispatch order equals record order — then the
core is shut down and joined, and its exit Snap is read back. The replay core runs no strategy
hook: only a strategy's already-decided orders need to reproduce, and those are on disk. When the
base Snap came from a mounted-strategy session, replay_from does mount an InertStrategy —
every hook at its no-op default, and never invoked, since the tail pump sends no market data —
solely so spawn_core's mount-presence derivation of collect_applied_fills, a field the hash
does cover, matches the source rather than diverging on a perfectly faithful fold.
What replays and what is neutral
The tail extraction is an exhaustive match with no catch-all — a new record
variant fails to compile until someone decides. Four variants re-apply: Cmd, StrategySubmit
(as an order command through the same apply_intent site), and ConditionalFire and
MarginCallLiquidate, each as a submit whose coid is still empty, so the replay core re-mints the
identical id from the restored generator. The trigger and the breach are never re-evaluated; the
replay core pumps no market data, so its equity never moves. Everything else is replay-neutral at
any position: Snap is the base, MintedSubmit's mint is re-derived, PortfolioSnap carries no
engine state, the arm and disarm records only resolve terms, and GtdExpire and ScheduleFire
are decision markers whose effects arrive as their own records — re-issuing them would double a
cancel or a set of submits. Market data is never journaled, and neither is what a strategy's hooks
saw; what is journaled is what they decided.
The three fences
The engine fence is vike_exec::state_hash, defined in crates/vike-exec/src/engine_snapshot.rs:
an FNV-1a64 over the canonical JSON of the engine snapshots, with two fields cleared before
hashing — the top-level now_ms, which is stamped by every ingest message including
non-journaled market ticks, and the account's mark-price cache, which mark ticks mutate. Neither
is reproducible by re-folding the exec lane alone, so hashing them would fail the fence on a
perfectly valid journal; everything else stays hashed. The fence verifies the recoverable order
and account state, which is the point. A mismatch is ReplayError::HashMismatch.
That covered one thing while the restore handed back three more, so replay_offline adds two
fences over the same oracle, the source journal's last Snap. conditionals_hash compares the
reproduced resting conditional books with the recorded ones, clearing a trailing arm's ratcheted
extreme, which moves on non-journaled market data as the marks do; it is gated on version 8 or
later through latest_segment_version, because an older Snap reads back empty books whether or
not the live session had arms. And the two id counters are fenced as floors by fence_floor:
an overshoot only wastes ids, while an undercount means a restored session re-mints an id the
pre-crash run already spent — and an arm id is the disarm key, so a duplicate lets a stale disarm
remove a fresh stop. Both surface as ReplayError::RestoreMismatch naming the field, distinct
from the engine mismatch on purpose: the two have different triage paths.
A truncated journal is reported as truncated, not as a divergence
When the frame walk halts at a torn frame, the last readable Snap is a mid-session checkpoint,
so the honestly re-folded tail cannot match it. That is ReplayError::Truncated carrying the
CorruptCause from Frames: a torn length prefix is the
ordinary consequence of a crash, a checksum-valid frame that does not parse came from a different
build. A truncated journal whose fences all pass — a tear strictly after the exit Snap — still
succeeds. Two shapes are Unsupported: a multi-engine base, whose extra engines' seed cash is not
in their snapshots, and a tail carrying the wall-clock waker — journaled only when the stuck-order
watchdog or either dead-man's switch is enabled, precisely so such a session refuses. A journal with
no Snap is Empty.
The restore
restore_from_journal returns a RestoredState: the engines, the coid session and counter to
resume so post-restart ids never collide with a still-open prior id, the arm counter, the resting
conditional books, the OTO/OCO contingency book, the per-mount attribution ledgers, and the
coid-to-mount origin map. Membership of the conditional books is a pure fold of the journal's
records (fold_conditionals), never the replay core's own book — that core neither fires nor
arms a trailing stop faithfully, the records do. The contingency book is read from the replay
core's exit Snap instead, because it is driven purely by journaled fills. The origin map is
folded from the StrategySubmit and MintedSubmit provenance already on disk (fold_coid_mounts),
which is what lets an order still resting across the restart book its eventual fill into the mount
that placed it. The caller pattern on its doc: rebuild the engine from the snapshot with the real
client, spawn the core with the restored session and books, point the journal at the same
directory so appends resume, and let venue reconciliation
repair whatever the venue saw while the process was down. Two residuals are declared: a trailing
arm re-arms at its last-snapshot extreme, slightly looser than it died, and re-tightens as live
data resumes; and on an unclean crash the attribution ledgers are the base Snap's value, the gap
showing in the published residual row rather than vanishing.
The tool
crates/vike-core/src/bin/replay_journal.rs is replay_offline at a command line:
cargo run -p vike-core --bin replay_journal -- <journal-dir>It prints the source journal's total record count, the number of snapshots verified (one — the
final hash), the reproduced hash, and one line per engine with open orders, positions and balance;
a failure prints REPLAY FAILED with the error and exits 1. The replay core's own journal is a
fresh directory under the system temp directory, removed on every path including a panic. The
binary page and the
determinism tutorial run it end to end.
Reading
The offline reader half — read_since and the durable checkpoint a materializer resumes with, the full read that halts at the first corrupt frame, and the rule that no reader ever takes the writer's lock.
Locking
The duplicate-instance interlock — one exclusive advisory lock on a sentinel file per journal directory, taken before any segment is mapped, released by the operating system on any exit, and never taken by a reader.