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.
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.
Events
The tagged union every part of Vike Trader speaks, and the three rules — emitter split, no silent vanish, exactly one terminal — that every venue adapter is machine-checked against.