vike
ConceptsThe command journal

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.

On this page