vike
ConceptsThe command journal

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.

crates/vike-core/src/journal_lock.rs states the contract in one line: at most one writer may hold a journal directory at a time — one CommandJournal, in one process. Vike Trader is a desktop product, so a double launch is not hypothetical: a second shortcut click, an operator restarting before the old process has died, a stray cargo run. Two writers on one journal is silent corruption in four distinct ways, each named on the module: interleaved frames break the replay determinism fence; the two client-order-id generators resume the same session and mint duplicate ids; two mounted makers quote against each other on one venue account; and both reconcile drivers fold the same divergence twice.

The mechanism

JournalLock::acquire takes an exclusive advisory lock through std::fs::File::try_lock on a sentinel file, <journal_dir>/LOCK (LOCK_FILE). On Unix that is flock(LOCK_EX | LOCK_NB); on Windows it is LockFileEx with the exclusive and fail-immediately flags. On both platforms the lock is keyed on the open file description or handle, not on the process — so a second CommandJournal in the same process is refused exactly like a second process, which is what makes the interlock unit-testable at all. The module is careful to call that a property of those two primitives rather than a universal one: a port to a target whose try_lock is backed by POSIX fcntl record locks would key on the process instead, and would have to re-verify the paragraph rather than inherit it. It is deliberately standard-library only — no new dependency, no unsafe, no platform fork — because the crate is unsafe_code = "deny" with only a small audited set of carve-outs (the journal's two, the segment mapping and the block reservation, plus the counter mirror's writer and reader maps in crates/vike-core/src/counters.rs), and the lock must not widen that set.

The sentinel is opened read-and-write, never append, because Windows refuses to lock an append-opened handle; and without truncation, because the file's bytes are meaningless but a file another process currently holds must never be rewritten.

Failure is fast and loud

A refused acquire is an io::ErrorKind::AddrInUse error whose message names the directory and the lock file and says what to do about it:

another vike instance is already writing the journal directory … (lock file …). Exactly one process may write a journal: two writers interleave WAL frames (replay's determinism fence then fails), re-mint the same client order ids, and double-fold reconciliation. Close the other instance, or point this one at a different journal directory.

Never a silent second writer, never a bare panic. Any other failure — the sentinel could not be created or opened, or the platform refused the lock — is surfaced verbatim rather than degraded into "no lock".

A leftover LOCK file is never stale

The guard is held for the writer's lifetime and releases when the file handle closes, which is documented std::fs::File behaviour. That covers a hard crash: the operating system closes the handle, so the file persisting on disk carries no state — only the live lock does. No cleanup or unlink is attempted, because unlinking a lock file is a race, not a tidy-up. The module's test the_lock_releases_on_drop_and_a_leftover_sentinel_is_not_stale pins exactly that: the sentinel survives, and the next instance acquires cleanly.

Where the writer takes it, and how it survives a roll

CommandJournal::open in crates/vike-core/src/journal/writer.rs acquires the lock first, before any segment is scanned or mapped, so a refused second instance never touches the live journal's files — let alone extends or restamps one — and a failed open leaves no lock held. The guard then has to survive every segment roll: map_segment is a segment constructor, not a directory-writer constructor, and returns a value with no lock (re-acquiring inside it would self-conflict, since the process already holds the directory), so roll moves the guard from the superseded value to its successor. Without that move the lock would be released on the first roll and a second instance could slip in mid-session. The move happens only after the new segment mapped successfully, so a failed roll leaves the lock exactly where it was. The syncer thread crosses the roll the same way — Segments and Sync policy carry those halves.

Only the write path locks

The scope is deliberately narrow. 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, exactly as before the interlock existed. The sentinel is invisible to all of them: every reader selects segments by the journal- prefix and .vjl suffix, and LOCK matches neither. Two different directories lock independently — the interlock is per journal directory, not global — which is the last of the module's four tests.

On this page