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.
crates/vike-core/src/journal/read.rs is the offline half of the journal: read_since, the
incremental tail-read a materializer resumes from, and MaterializeCheckpoint, the small durable
file it resumes with. Neither takes the journal directory's writer lock, and neither runs on the
fold thread. The full read they build on, CommandJournal::read_all, lives with the writer in
crates/vike-core/src/journal/writer.rs and is what replay
consumes.
read_all
read_all returns every valid record across every segment, in seq order, walking the segments by
index. A file without the magic is skipped; a file with it has its version gated before a frame
is read. It stops at the first torn record — journal-wide, so later segments past a corruption gap
are never returned — and read_all_reporting is the same read with the halt cause carried out
instead of discarded, logged exactly once per call with the segment, the offset and the reading.
Frames carries the three causes. latest_segment_version
answers one narrow question beside it: the format version stamped on the highest-indexed segment,
which is the shape the journal's last Snap was written under. It exists for a single consumer,
replay's conditional-book fence, which must tell "the field was absent" from "the field was
present and empty" for a field that was defaulted in at version 8.
read_since
read_since(dir, after_seq) returns every valid record whose seq is strictly greater than
after_seq, in seq order. The bound is strict because after_seq is the last seq already
consumed, which is the resume semantic a checkpoint needs. The implementation is the simplest
correct one — read_all, then filter by record_seq — and it is O(journal): every segment is
read and deserialized. That stays bounded because the journal is snapshot-compacted;
prune_before_latest_snap drops fully superseded segments,
so the live journal never grows without limit. A streaming reader that seeks to after_seq is a
declared later optimization, not needed for correctness.
The first record is seq 0, and read_since(dir, 0) skips it
A cold-start consumer that has never checkpointed loads 0 and, with a strict lower bound, never
sees the seq-0 record. In practice the materializer is seeded from the latest Snap, whose seq is
its exclusive base, so this is benign — but a caller that must include seq 0 keys off
MaterializeCheckpoint::exists, which distinguishes "no checkpoint yet" from "materialized up to
seq 0", and chooses a full read.
The checkpoint
MaterializeCheckpoint is <dir>/materializer.ckpt: the last seq a materializer has consumed,
so it can resume its tail across process restarts. load returns the stored seq, or 0 when the
file is absent or unparseable — it never errors, and a consumer that finds no checkpoint simply
re-materializes from the beginning of the compacted journal. store writes a temp file, fsyncs it
and renames it into place, so a torn write leaves the old checkpoint intact rather than a
half-written seq; the rename replaces the destination atomically on both Unix and Windows. The
file sits beside the segments and is independent of them: replay and restore never read it, and
pruning never modifies it — though the prune does honour it as a retention floor, keeping any
segment that holds records the materializer has not consumed.
The materializer that resumes from it
The consumer these two were built for is vike_ops::journal_mat's JournalMaterializer, in
crates/vike-ops/src/journal_mat.rs. It is a strict off-fold tail-follower: it drains the tail on
a cadence measured in seconds (five by default), and materializes fills into the
history store's execution trade log and order lifecycles into its
order log, so reporting and reconciliation read a columnar store instead of re-scanning the
journal. Durability is at-least-once — the checkpoint plus the store's commit-key idempotency — so
a crash and resume re-processes only the un-materialized tail and never double-appends.
Its header states the rule any journal reader that derives state must follow. Because the journal
is write-ahead, it contains events the live state machine went on to refuse, so the materializer
folds every lifecycle event through the real ManagedOrder::apply — the same guarded mutator the
live engine folds through — and owns no transition table of its own. The concrete case it used to
get wrong was an OrderModified arriving on an already-terminal order, an amend acknowledgement
racing the fill that terminalized it: the live order kept its terms, and the old copy appended a
row carrying a quantity the order never had. The MintedSubmit record on
Records exists for this reader, tying a server-minted coid to
its terms so an order that terminalizes without ever filling still lands in the log. The declared
bound on a restart is staleness, never corruption: the tracker rebuilds empty from the
post-checkpoint tail, and an order open across the restart may carry a slightly stale snapshot
until its next resolved event.
Readers never lock
read_all, latest_segment_version, read_since and prune_before_latest_snap take no lock,
so offline inspection and replay tooling keep working against a directory a live core owns. The
LOCK sentinel is not a .vjl file and is invisible to every scanner —
Locking covers the writer side. What a reader sees is the page
cache, not the disk: a materializer or a restart reading these segments with std::fs::read
observes every appended byte whether or not the syncer has forced it out yet, which is one half of
why the sync policy is latency shaping rather than
durability. Two more readers are named on JournalRecord's own contract as single-variant lookups
deliberately left as catch-alls: the incident tool's
latest-Snap lookup, and vike_report::journal_read::fills_from_journal.
Sync policy
Every blocking msync the journal performs runs on one dedicated syncer thread the appender never joins, hand-off is a monotonic watermark rather than a queue, and the append side pays one store, one swap and at most one channel send — the fold-path latency contract, with the measurements behind it.
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.