pub struct OplogJournal { /* private fields */ }Expand description
Append-only JSONL journal for OpRecords. Holds an exclusive advisory
lock on <path>.lock for its lifetime — one writer per journal path.
Implementations§
Source§impl OplogJournal
impl OplogJournal
Sourcepub fn open(path: &Path) -> Result<Self>
pub fn open(path: &Path) -> Result<Self>
Open (creating parents and the file if needed) for appending. Existing content is preserved — append mode, never truncate.
If the file’s last line is torn (a crash mid-write left it without a terminating newline), a newline is written first so the next append starts a fresh line instead of gluing itself onto the garbage — otherwise the first post-crash append would be lost with the tail.
Sourcepub fn append(&mut self, op: &OpRecord) -> Result<()>
pub fn append(&mut self, op: &OpRecord) -> Result<()>
Append one op as a single JSON line and flush it to the OS page cache, so a subsequent crash can tear at most the next record.
Not a durability barrier — flush clears the BufWriter buffer
to the OS but does not fsync. A caller with a
journal-durable-before-transmit / ack-asserts-durable contract
(the sync session) MUST call OplogJournal::sync before it
transmits or acks; per-op fsync would be needless latency (one batch
barrier at the contract point is enough).
Failure recovers the writer. On a write/flush error the
BufWriter would otherwise retain the unflushed line; a later
append would then emit that rolled-back line beside the caller’s
re-minted same-seq op — a permanent chain fork. So a failed append
recreates the writer on a fresh append-mode handle, discarding
the poisoned buffer, before returning the error (the caller rolls
its in-memory chain back to the durable ops). Bytes that already
reached the file are at most one torn trailing line, which
OplogJournal::load tolerates.
Sourcepub fn sync(&mut self) -> Result<()>
pub fn sync(&mut self) -> Result<()>
Durability barrier: flush the buffer and fsync the journal file to
stable storage. The sync session crosses this before it transmits an
op (journal-durable-before-transmit, B1 MUST) and before it acks a
fold frontier (ack-asserts-durable, B4 MUST) — one batch call, not
per-op.
pub fn path(&self) -> &Path
Sourcepub fn truncate_to(
&mut self,
ops: &[OpRecord],
checkpoint_hash: &str,
) -> Result<()>
pub fn truncate_to( &mut self, ops: &[OpRecord], checkpoint_hash: &str, ) -> Result<()>
Rewrite the journal to exactly ops, stamped with a
TruncationMarker naming checkpoint_hash as its first line —
B4’s truncation-below-a-frontier step, executed under the advisory
lock this open journal already holds (no second writer can
interleave). The marker travels IN the rewritten file, so it is
atomic with the truncation: there is no crash window in which the
journal is truncated but unmarked (or marked but untruncated).
Durability: the retained ops are written to a sibling temp file,
fsync’d, and atomically renamed over the journal, so a crash at any
point leaves either the old complete journal or the new complete
tail — never a partial rewrite. Crash-ordering invariant (callers
MUST honor it; crate::compact::compact_and_truncate does by
construction): the covering checkpoint is durable BEFORE this runs.
Truncation makes the dropped ops unrecoverable from the journal; the
checkpoint named by the marker is what still accounts for them.
Sourcepub fn load(path: &Path) -> Result<Vec<OpRecord>>
pub fn load(path: &Path) -> Result<Vec<OpRecord>>
Load a full (never-truncated) journal: every parseable op, in file order. Blank and unparseable (torn) lines are skipped; a missing file is an empty log (a fresh device bootstraps from nothing).
A journal carrying a TruncationMarker is refused with a
runtime error: its ops are only the retained tail, and treating them
as the whole log silently re-mints truncated seqs on
DeviceLog::resume — the permanent chain fork. Use
OplogJournal::load_with_marker + the named checkpoint +
crate::checkpoint::resume_anchored instead.
Sourcepub fn load_with_marker(
path: &Path,
) -> Result<(Option<TruncationMarker>, Vec<OpRecord>)>
pub fn load_with_marker( path: &Path, ) -> Result<(Option<TruncationMarker>, Vec<OpRecord>)>
Load a journal that may have been truncated: the
TruncationMarker (if any) plus every parseable op, in file
order. Blank and unparseable (torn) lines are skipped; a missing
file is (None, []). When the marker is Some, the ops are a
retained tail — anchor them on the named checkpoint
(crate::checkpoint::verify_anchored) and resume via
crate::checkpoint::resume_anchored, never DeviceLog::resume.