Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
134 commits
Select commit Hold shift + click to select a range
26e4e71
cmux-journal: one append-only journal per daemon, with group commit a…
lawrencecchen Nov 3, 2026
36f09c8
cmux-journal: type a test range; drop an unread segment field (fleet …
lawrencecchen Nov 3, 2026
f55f8c5
cmux-journal: a failed F_FULLFSYNC is an error, not a plain fsync; no…
lawrencecchen Nov 3, 2026
0027391
cmux-journal: the past-the-end test reads back the storage it wrote to
lawrencecchen Nov 3, 2026
89293fb
Merge feat-cmux-next (3a436460eaa1) into rewrite/r-durability-s1-jour…
lawrencecchen Nov 3, 2026
a936848
cmux-journal: a sync a signal interrupts is made again (macOS)
lawrencecchen Nov 3, 2026
b6c6030
cmux-journal: crash sim of write-back errors and crash in recovery; S…
lawrencecchen Nov 3, 2026
263da86
cmux-journal: the F2 regression damages the header of the last of sev…
lawrencecchen Nov 3, 2026
4dfdffc
cmux-journal: the crash sim forces the F4 order (failed sync, kill, O…
lawrencecchen Nov 4, 2026
3498975
cmux-journal: fix the S1 review findings F1-F7 (markers, recovery cop…
lawrencecchen Nov 3, 2026
45f6486
cmux-journal: split the crash sim's machine model into sim/machine.rs…
lawrencecchen Nov 4, 2026
644e77a
cmux-journal: re-review regressions G1-G4 and page eviction in the cr…
lawrencecchen Nov 4, 2026
cb893c6
cmux-journal: fix the re-review findings G1-G4 and G6
lawrencecchen Nov 4, 2026
936b8ac
cmux-journal: third-review regressions G7 (A, B) and N1 (red)
lawrencecchen Nov 4, 2026
db9eb7b
cmux-journal: fix the third-review findings G7, N1, N2; split recover…
lawrencecchen Nov 4, 2026
215c8f0
cmux-journal: report each loss once, also when one open finds it twic…
lawrencecchen Nov 4, 2026
4da4678
cmux-journal: fourth-review regressions G8, N3, N4 (red)
lawrencecchen Nov 4, 2026
b589044
cmux-journal: the quarantine's floor in its file names; DAMAGE before…
lawrencecchen Nov 4, 2026
9c47594
cmux-journal: sort the quarantine list with sort_by_key (clippy unnec…
lawrencecchen Nov 4, 2026
b4369cf
cmux-journal: Journal::on_written, a listener per written batch (for …
lawrencecchen Nov 4, 2026
0c10b5e
cmux-journal: Journal::retire_through, the one delete (segments a com…
lawrencecchen Nov 4, 2026
26d685b
acpmux: move MemoryStore and the store settings into their own files
lawrencecchen Nov 4, 2026
53e9334
acpmux: the event log's journal switch and its crash tests (red)
lawrencecchen Nov 4, 2026
2d66751
acpmux: the event log through one journal, behind `store.journal` (R-…
lawrencecchen Nov 4, 2026
00512a2
acpmux: journal store tests for the seal and a chat made again after …
lawrencecchen Nov 4, 2026
ab0e953
acpmux: journal store: the seal under the appends' lock; a replay kee…
lawrencecchen Nov 4, 2026
65cead0
cmux-journal: fifth-review regressions G9 (red)
lawrencecchen Nov 4, 2026
51cfdee
cmux-journal: name a skipped LSN floor in a FLOOR.<lsn> file (G9)
lawrencecchen Nov 4, 2026
61abef8
Merge the S1 head 51cfdee876b (G9: FLOOR.<lsn> files) into S2
lawrencecchen Nov 4, 2026
e030f4f
acpmux: journal store regressions from the S2 review (red)
lawrencecchen Nov 4, 2026
8b71b4f
acpmux: the checkpoint freeze test fails a log sync, not a folder ope…
lawrencecchen Nov 4, 2026
03fccde
acpmux: the journal store repairs logs, never rebuilds them (S2 review)
lawrencecchen Nov 4, 2026
92b52bf
acpmux: split the journal store under its size budget (a move)
lawrencecchen Nov 4, 2026
41d51e8
acpmux: the unrepairable-chat test waits for line 1 to be durable bef…
lawrencecchen Nov 4, 2026
2abbb1a
acpmux: replies and events wait for durability, tests (red, S2c)
lawrencecchen Nov 4, 2026
d68f794
acpmux: replies and events wait until what they show is durable (S2c)
lawrencecchen Nov 4, 2026
7a8bbdc
acpmux: the journal lock wait's deadline without an unchecked Instant…
lawrencecchen Nov 4, 2026
06e5b86
Merge the S2 head 7a8bbdce62c into S2c
lawrencecchen Nov 4, 2026
dcfecad
Merge feat-cmux-next d422b94b60d into S2 (#33919 landed there as da88…
lawrencecchen Nov 4, 2026
14c7390
Merge the S2 head dcfecade4df (feat d422b94b60d) into S2c
lawrencecchen Nov 4, 2026
80c5fb2
acpmux: a failed log sync stops the real daemon for a restart, test (…
lawrencecchen Nov 4, 2026
41f3969
acpmux: a daemon its store stopped for a restart exits with status 74
lawrencecchen Nov 4, 2026
62b4f68
acpmux: the restart test's request in flight goes through the acpmux …
lawrencecchen Nov 4, 2026
994f741
acpmux: the daemon's disk tier with the journal on, tests (red)
lawrencecchen Nov 4, 2026
f27e942
acpmux: the restart test borrows no &str twice (clippy needless_borro…
lawrencecchen Nov 4, 2026
350a9d7
acpmux: with the journal on, the daemon keeps the default disk tier (…
lawrencecchen Nov 4, 2026
a43e979
acpmux: an agent chunk is a Stream record, test (red)
lawrencecchen Nov 4, 2026
ce8dcba
acpmux: an agent chunk is a Stream record (the record's message names…
lawrencecchen Nov 4, 2026
d2c6386
acpmux: review 2 of #34901, tests (red): a crash model that follows r…
lawrencecchen Nov 4, 2026
84f9c26
acpmux: review 2 of #34901: a restart writes the replayed lines again…
lawrencecchen Nov 4, 2026
d9938eb
Merge the S2 review 2 fix 84f9c266975 into S2c
lawrencecchen Nov 4, 2026
3b245c1
Merge feat-cmux-next (b43ce2b4ae2) into S2
lawrencecchen Nov 4, 2026
3273b2a
Merge feat-cmux-next (d3d5a6246abc) into rewrite/r-durability-s2-revi…
lawrencecchen Nov 4, 2026
0505e9b
acpmux: chat metadata through the event journal, tests (red, R-DURABI…
lawrencecchen Nov 4, 2026
71bc09a
acpmux: chat metadata through the event journal (R-DURABILITY S3a)
lawrencecchen Nov 4, 2026
0280ac3
acpmux: S3a, tests (red): folders handed out of the store
lawrencecchen Nov 4, 2026
0e61748
acpmux: S3a red tests: a replay after a folder swap; flush_session's …
lawrencecchen Nov 4, 2026
cc9a560
acpmux: S3a: each metadata image is bound to its chat folder; session…
lawrencecchen Nov 4, 2026
d147057
acpmux: S2 follow-ups from review 3 of #34901, tests (red)
lawrencecchen Nov 4, 2026
49d7e5c
acpmux: S2 follow-ups from review 3 of #34901: a failed repair holds …
lawrencecchen Nov 4, 2026
5bb3a27
acpmux: a metadata change logs its line before its metadata, tests (r…
lawrencecchen Nov 4, 2026
db60828
acpmux: the qos journal tests module is macOS-only (its Linux build h…
lawrencecchen Nov 4, 2026
3916f84
acpmux: review of #35400 F1 and F2, tests (red)
lawrencecchen Nov 4, 2026
c553a19
acpmux: review of #35400 F1, the streaming read, test (red): session/…
lawrencecchen Nov 4, 2026
7ee04c0
acpmux: the test seam PAUSE_AFTER_LOG_WRITE is reachable from the ser…
lawrencecchen Nov 4, 2026
d328b8a
acpmux: review of #35400: a read covers the records of the lines it s…
lawrencecchen Nov 4, 2026
7a4f02a
acpmux: S3b red tests: the recording store implements delete (a requi…
lawrencecchen Nov 4, 2026
39f9580
threading-model: the acpmux event journal's threads and bounds (write…
lawrencecchen Nov 4, 2026
5cbf604
Merge the S2 head 3273b2ade1f (feat b43ce2b4ae2 and d3d5a6246ab) into…
lawrencecchen Nov 4, 2026
9cf1425
Merge feat-cmux-next (3dcd5db58a3) into S2c, after S2 (#34901) landed…
lawrencecchen Nov 4, 2026
3969cb0
Merge feat-cmux-next (35a1a909a11a) into rewrite/r-durability-s2c-rep…
lawrencecchen Nov 4, 2026
eb24c40
acpmux: the stall test's journal syncs take 3 s, not an hour (a sync …
lawrencecchen Nov 4, 2026
71d7e09
acpmux: a tags or rules change logs its line before its metadata (R-D…
lawrencecchen Nov 4, 2026
b2240f1
acpmux: an open that replays many chats flushes the drive a bounded n…
lawrencecchen Nov 4, 2026
15cfa14
acpmux: the journal store's checkpoint and recovery sync in write ord…
lawrencecchen Nov 4, 2026
d2d52e4
acpmux: the open after a kill syncs nothing per replayed chat, test (…
lawrencecchen Nov 4, 2026
6def18f
acpmux: the open-syncs test counts the opening thread's syncs only (t…
lawrencecchen Nov 4, 2026
b80145e
acpmux: the open after a kill syncs nothing per replayed chat; a free…
lawrencecchen Nov 4, 2026
ecd19d0
Merge the S2c head eb24c40e1df (as #35400 landed) into S3a
lawrencecchen Nov 4, 2026
742a8d5
Merge feat-cmux-next (a0d32a7b4e8) into S3a after S2c (#35400) landed…
lawrencecchen Nov 4, 2026
edca3b4
acpmux: the idle checkpointer's period, test (red): a change seen by …
lawrencecchen Nov 4, 2026
5279af4
acpmux: the idle checkpointer's period ends only on its own state, no…
lawrencecchen Nov 4, 2026
9d6c45e
acpmux: S3a review (#35483) red tests: switch-off metadata and delete…
lawrencecchen Nov 4, 2026
44b1b45
acpmux: S3a review (#35483) red tests: a new chat is acknowledged onl…
lawrencecchen Nov 4, 2026
0d24636
acpmux: S3a review (#35483) fixes: the replay never makes a folder ag…
lawrencecchen Nov 4, 2026
75550ba
Merge the S3a review fix 0d246364d90 (with the S2c head and feat) int…
lawrencecchen Nov 4, 2026
2387030
acpmux: review of #35562 red tests: a failed repair holds no other ch…
lawrencecchen Nov 4, 2026
3b7007c
acpmux: review of #35562: a failed repair keeps its chat's lines in a…
lawrencecchen Nov 4, 2026
4d47b84
Merge the S2 follow-ups head 3b7007c175d (the S3a fix, the race fix, …
lawrencecchen Nov 4, 2026
3fc1c15
Merge feat-cmux-next (78fcd7c82571) into rewrite/r-durability-s3-meta
lawrencecchen Nov 4, 2026
05cb007
Merge the S3b head 4d47b845dea (the S3a fix, the S2 follow-ups' repai…
lawrencecchen Nov 4, 2026
81a1a14
Merge feat-cmux-next (4546c99a6c1d) into rewrite/r-durability-s3-meta
lawrencecchen Nov 4, 2026
668b6cc
Merge the S3a head 81a1a14fa28 (feat 4546c99a6c1, with #35845) into t…
lawrencecchen Nov 4, 2026
de96183
Merge feat-cmux-next (7427100d5d06) into rewrite/r-durability-s2-foll…
lawrencecchen Nov 4, 2026
8301994
Merge feat-cmux-next (7427100d5d06) into rewrite/r-durability-s3-meta
lawrencecchen Nov 4, 2026
f269412
Merge the S3a head 8301994b135 (feat 7427100d5d0) into the S2 follow-ups
lawrencecchen Nov 4, 2026
66e1df5
Merge the S2 follow-ups head f2694125d59 (feat 7427100d5d0) into S3b
lawrencecchen Nov 4, 2026
b8159d9
Merge the S3b head (feat 7427100d5d0, with #35845) into recovery-syncs
lawrencecchen Nov 4, 2026
7c8cb95
acpmux: S3a delta review (N1) red tests: a change to a chat whose ses…
lawrencecchen Nov 4, 2026
fc83b32
acpmux: S3a delta review (N1): journalLsn is bound to its journal's i…
lawrencecchen Nov 4, 2026
dbd1e79
Merge the S3a N1 fix fc83b326fba (journalLsn bound to the journal id)…
lawrencecchen Nov 4, 2026
7fc15b7
Merge the S2 follow-ups head dbd1e79737a (the S3a N1 fix) into S3b
lawrencecchen Nov 4, 2026
b0aee12
Merge the S3b head 7fc15b776ef (the S3a N1 fix) into recovery-syncs
lawrencecchen Nov 4, 2026
d860bef
acpmux: recovery syncs, test (red): a retention rewrite of a checkpoi…
lawrencecchen Nov 4, 2026
f9c6a16
acpmux: recovery syncs: a retention rewrite of a checkpointed chat ru…
lawrencecchen Nov 4, 2026
05493ca
acpmux: review of #35866 red tests (the reviewer's): a failed checkpo…
lawrencecchen Nov 4, 2026
f0ebe9e
acpmux: review of #35866, P1-1: a failed checkpoint keeps the segment…
lawrencecchen Nov 4, 2026
101232e
acpmux: review of #35866, P2-1 and P2-2: a freeze is a journal record…
lawrencecchen Nov 4, 2026
24cc4ae
acpmux: apply names the replay's frozen field (read at the open into …
lawrencecchen Nov 4, 2026
bf2f48d
Merge feat-cmux-next (07c879aab80, with S2fu #35562 squashed) into S3b
lawrencecchen Nov 4, 2026
6a5a6d1
Merge feat-cmux-next (ecfe8d94e9d0) into rewrite/r-durability-s3b-order
lawrencecchen Nov 4, 2026
0befb17
Merge the S3b head 6a5a6d138c9 (feat with S2fu #35562 squashed) into …
lawrencecchen Nov 5, 2026
60cce5e
Merge feat-cmux-next (b536ff861c5, with #35895) into recovery-syncs
lawrencecchen Nov 5, 2026
df1815e
acpmux: a checkpoint and another thread's frozen record, test (red)
lawrencecchen Nov 5, 2026
9e90c42
acpmux tests: the frozen-record test drives the queue-full retry (red)
lawrencecchen Nov 5, 2026
f62feba
acpmux: a checkpoint waits for a failed sync's freeze, not for its fr…
lawrencecchen Nov 5, 2026
4a883c4
acpmux: a refused frozen record, tests (red; delta review of #35866, D1)
lawrencecchen Nov 5, 2026
32fd1f8
acpmux tests: arm the shutdown's failing log sync before the seal (re…
lawrencecchen Nov 5, 2026
1617013
acpmux: the frozen record is a control record, so a full queue never …
lawrencecchen Nov 5, 2026
7b27d29
acpmux: the journal closes only after the shutdown's last checkpoint …
lawrencecchen Nov 5, 2026
510cade
acpmux: a frozen record the journal still refuses, test (red; D1, fai…
lawrencecchen Nov 5, 2026
b398813
acpmux: a frozen record the journal refuses leaves a durable marker (…
lawrencecchen Nov 5, 2026
a2475c7
acpmux: the store's docs and the P2 review tests describe the freeze …
lawrencecchen Nov 5, 2026
9125b99
acpmux: a kill while the frozen record waits in the queue, test (red;…
lawrencecchen Nov 5, 2026
dcf9ed7
acpmux: a kill after a failed log close and before any checkpoint, te…
lawrencecchen Nov 5, 2026
2a065d2
acpmux: the checkpoint writes back the log ranges the open verified, …
lawrencecchen Nov 5, 2026
6b0547a
acpmux: JournalStore::seal's doc comment starts with what the seal do…
lawrencecchen Nov 5, 2026
2e36ab0
Merge feat-cmux-next (1b585e3ddca, with #35963) into recovery-syncs
lawrencecchen Nov 5, 2026
7f3e036
Merge feat-cmux-next (cd9b41667851) into rewrite/r-durability-recover…
lawrencecchen Nov 5, 2026
b53a750
acpmux: a deleted or replaced chat's log never takes the bytes the op…
lawrencecchen Nov 5, 2026
4846ec2
acpmux: the open writes the verified log bytes back; the checkpoint o…
lawrencecchen Nov 5, 2026
ff69226
acpmux: the write-back tests take std::io::Write from the store modul…
lawrencecchen Nov 5, 2026
3452068
acpmux: the checkpoint syncs the open's written-back segments by path…
lawrencecchen Nov 5, 2026
29967f9
acpmux: the open keeps the written-back segments open, up to a cap, f…
lawrencecchen Nov 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmux-tui/crates/acpmux/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ pub(crate) fn private_state_file(path: &Path) -> Result<std::fs::File> {
}

#[cfg(unix)]
fn validate_private_state_file(file: &std::fs::File, path: &Path) -> Result<()> {
pub(crate) fn validate_private_state_file(file: &std::fs::File, path: &Path) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};

let metadata = file.metadata()?;
Expand Down
12 changes: 11 additions & 1 deletion cmux-tui/crates/acpmux/src/owner_only.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,21 @@ pub(crate) fn open_read(path: &Path) -> io::Result<File> {
/// Open or create a state file for read/write without following a reparse
/// point, narrowing a wider DACL before it can be used as daemon.lock.
pub(crate) fn open_rw(path: &Path) -> io::Result<File> {
open_rw_with(path, OPEN_ALWAYS)
}

/// [`open_rw`] of a state file that must exist already, never made here (a
/// log segment the journal store writes in place).
pub(crate) fn open_rw_existing(path: &Path) -> io::Result<File> {
open_rw_with(path, OPEN_EXISTING)
}

fn open_rw_with(path: &Path, disposition: u32) -> io::Result<File> {
use std::os::windows::fs::MetadataExt;

let file = open_owner_only(
path,
OPEN_ALWAYS,
disposition,
GENERIC_READ | GENERIC_WRITE | 0x80 | WRITE_SECURITY_ACCESS,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
)?;
Expand Down
4 changes: 3 additions & 1 deletion cmux-tui/crates/acpmux/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1831,7 +1831,7 @@ fn sync_directory(path: &Path) -> Result<()> {
.read(true)
.custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW)
.open(path)?;
dir.sync_all().map_err(SyncFailed)?;
log_sync::sync_full(&dir).map_err(SyncFailed)?;
#[cfg(test)]
sync_model::folder_synced(path);
Ok(())
Expand Down Expand Up @@ -2139,6 +2139,8 @@ mod journal_meta_tests;
mod journal_store_tests;
#[cfg(all(test, unix))]
mod journal_sync_tests;
#[cfg(all(test, unix))]
mod journal_write_back_tests;

impl LocalStore {
/// `Store::scan`, and with `strict` `Store::scan_strict`.
Expand Down
625 changes: 625 additions & 0 deletions cmux-tui/crates/acpmux/src/store/journal_meta_tests.rs

Large diffs are not rendered by default.

159 changes: 140 additions & 19 deletions cmux-tui/crates/acpmux/src/store/journal_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@
//! retires the journal segments before it. A failed log sync (in it, at a log's close, or in
//! `flush_session`) freezes it: a later sync may report success for pages the kernel dropped,
//! so the journal keeps every line and the store asks the daemon to stop
//! ([`Store::stop_reason`]); the next start's replay writes the logs again. Ten failed
//! checkpoints in a row ask the same.
//! ([`Store::stop_reason`]). The freeze is a `frozen` record in the journal (a control record
//! that a full queue never refuses; the journal closes only after the shutdown's last
//! checkpoint): the open that replays it writes every replayed chat's lines again. A record the
//! journal still refuses (its writer stopped) leaves a durable marker, `<home>/journal-frozen`,
//! that the open reads the same way. Ten failed checkpoints in a row ask the daemon to stop
//! too.
//! - Recovery (open) repairs each log and never deletes a whole line or a folder: see
//! [`Inner::repair`]. It writes every replayed chat's lines again from the point the journal
//! takes over, so the first checkpoint after it syncs bytes this process wrote (a killed
Expand Down Expand Up @@ -63,22 +67,32 @@ mod meta;
mod new_folders;
mod repair;

use checkpoint::{read_checkpoint, remove_checkpoint_temporaries};
use checkpoint::{frozen_marker, read_checkpoint, remove_checkpoint_temporaries};
pub(super) use fenced::FencedStorage;
#[cfg(test)]
pub(super) use meta::SETTLE_HOLD;
use repair::Replay;
#[cfg(test)]
pub(super) use repair::STOP_AFTER_CUT;
pub(super) use repair::{FAIL_NEXT_WRITE_BACK, HELD_CAP, STOP_AFTER_CUT};

/// The journal record kinds. (Kind 3, a `cancel` of a line the log refused, was used before the
/// review of #34901 and never shipped: a refused line is never journaled now.)
/// review of #34901 and never shipped: a refused line is never journaled now. Kind 4 is `meta`,
/// `journal_store/meta.rs`; kind 5 was planned for S3 and never used.)
const KIND_LOG: u16 = 1;
const KIND_FORGET: u16 = 2;
/// A freeze (a failed sync of the store's files; its reason as text): the open that replays it
/// writes every replayed chat's lines again (`Inner::freeze`).
const KIND_FROZEN: u16 = 6;

const JOURNAL_DIR: &str = "journal";
const CHECKPOINT_NAME: &str = "journal-projected.json";
/// The checkpoint file's schema.
const CHECKPOINT_SCHEMA: &str = "acpmux.journal-projected.v1";
/// A durable marker of a freeze whose `frozen` record may not have reached the journal (refused,
/// or not durable when the journal closed): the next open writes every replayed chat's lines
/// again, as for the record (fail closed; delta review of #35866, D1;
/// `journal_store/checkpoint.rs`).
const FROZEN_NAME: &str = "journal-frozen";
/// Where the journaled lines of a log that recovery leaves as it is go.
const UNAPPLIED_DIR: &str = "journal-unapplied";
const CHECKPOINT_EVERY: Duration = Duration::from_secs(30);
Expand Down Expand Up @@ -148,6 +162,20 @@ thread_local! {
/// the LSN and the chats to sync.
pub(crate) static HOLD_AFTER_START: log_sync::HoldSlot =
const { std::cell::RefCell::new(None) };
/// Tests: the next `frozen` record a freeze on this thread appends is refused, as when the
/// journal's writer stopped (`AppendError::Closed`).
pub(crate) static FAIL_FROZEN_RECORD: std::cell::Cell<bool> =
const { std::cell::Cell::new(false) };
/// Tests: a freeze on this thread stops right before it writes its fallback marker (off any
/// sync's count).
pub(crate) static HOLD_BEFORE_FROZEN_MARKER: log_sync::HoldSlot =
const { std::cell::RefCell::new(None) };
}

/// Tests: the stop of [`HOLD_BEFORE_FROZEN_MARKER`].
#[cfg(test)]
fn hold_before_frozen_marker() {
log_sync::hold(&HOLD_BEFORE_FROZEN_MARKER);
}

/// Tests: the stop of [`HOLD_AFTER_START`].
Expand Down Expand Up @@ -283,12 +311,40 @@ struct Order {
/// Each chat's newest metadata image that its `session.json` does not hold yet (S3a,
/// `journal_store/meta.rs`): reads see it first, the checkpoint writes it.
metas: BTreeMap<String, meta::Pending>,
/// The log segments whose verified bytes the open's repair wrote back over themselves: the
/// next checkpoint syncs them before it retires their records. A killed daemon's writes may
/// be in the page cache only, and a previous run's failed sync may have dropped pages that
/// still read back; the write marked the range dirty again, so the sync puts the bytes on the
/// disk whatever happened before (delta review 2 of #35866, D2). The checkpoint only syncs:
/// a file put at the path since is never written (delta review 3, D3). A delete drops its
/// chat's.
read_segments: BTreeMap<PathBuf, repair::WrittenBack>,
/// How many more written-back segments the open may keep open (`repair::held_cap`).
held_left: usize,
/// The open replayed a `frozen` record (a previous run's failed sync) or found the freeze
/// marker: its repair writes every replayed chat's lines again.
rewrite_all: bool,
/// The open found the freeze marker (`FROZEN_NAME`): the first checkpoint that succeeds
/// removes it.
marker: bool,
/// Chats whose lines the open took from a repair file (`journal_store/repair.rs`): written
/// again too, as their earlier repair could not write them.
rewrite: BTreeSet<String>,
/// An upper bound of the LSN of each chat's newest log record in this process (an append, or
/// the open's replay and repair). A chat without one has no log record past the checkpoint
/// the open started from: a retention rewrite of a chat at or below the checkpoint needs no
/// checkpoint of its own (`Store::commit_compaction`).
log_lsn: HashMap<String, Lsn>,
}

impl Order {
/// Whether a checkpoint has anything to do.
fn has_work(&self) -> bool {
self.applied > self.checkpointed || !self.dirty.is_empty() || !self.metas.is_empty()
self.applied > self.checkpointed
|| !self.dirty.is_empty()
|| !self.metas.is_empty()
|| !self.read_segments.is_empty()
|| self.marker
}
}

Expand All @@ -309,6 +365,7 @@ impl JournalStore {
remove_checkpoint_temporaries(home);
let local = LocalStore::new(home.join("sessions"), config.segment_bytes)?;
let from = read_checkpoint(home)?;
let marker = frozen_marker(home);
let (journal, report, mut replay) =
open_journal(storage, &home.join(JOURNAL_DIR), journal_config, from)?;
for damage in &report.damage {
Expand Down Expand Up @@ -343,6 +400,9 @@ impl JournalStore {
order: Mutex::new(Order {
checkpointed: from,
damage: !report.damage.is_empty(),
rewrite_all: replay.frozen || marker,
marker,
held_left: repair::held_cap(),
..Order::default()
}),
checkpointing: Mutex::new(()),
Expand All @@ -360,8 +420,11 @@ impl JournalStore {
// The lines of chats whose repair failed at an earlier open, journaled again first.
inner.take_repairs(&mut replay);
inner.apply(replay)?;
if let Err(error) = inner.checkpoint() {
tracing::warn!("event journal checkpoint after the replay: {error:#}");
// The checkpoint after the replay runs on the checkpointer's thread at once, not here: the
// daemon binds its socket after this open returns, and the checkpoint's syncs are the
// open's cost per replayed chat (6.1 s for 300 chats on a loaded Mac, fleet 59175d6b).
if inner.order.lock_or_recover().has_work() {
inner.timer.lock_or_recover().wanted = true;
}
let store = Self {
inner,
Expand Down Expand Up @@ -398,6 +461,19 @@ impl JournalStore {
pub(super) fn opened(&self) -> &cmux_journal::OpenReport {
&self.opened
}

/// Tests: the written-back segments listed for the next checkpoint (`Order::read_segments`):
/// how many the open keeps open, and how many it closed.
#[cfg(test)]
pub(super) fn written_back(&self) -> (usize, usize) {
let order = self.inner.order.lock_or_recover();
let held = order
.read_segments
.values()
.filter(|written| matches!(written, repair::WrittenBack::Held(_)))
.count();
(held, order.read_segments.len().saturating_sub(held))
}
}

/// [`super::open`] with the journal on `storage` (a simulated file system in the crash tests) and
Expand Down Expand Up @@ -547,6 +623,13 @@ impl Store for JournalStore {
inner.acked.store(lsn, std::sync::atomic::Ordering::SeqCst);
}
order.applied = lsn;
// A chat's first line in this process allocates its entry; every later one reuses it.
match order.log_lsn.get_mut(id) {
Some(newest) => *newest = lsn,
None => {
order.log_lsn.insert(id.to_owned(), lsn);
}
}
order.bytes = order.bytes.saturating_add(u64::try_from(payload.len()).unwrap_or(u64::MAX));
let full = order.bytes > CHECKPOINT_BYTES && !order.frozen;
drop(order);
Expand Down Expand Up @@ -646,14 +729,31 @@ impl Store for JournalStore {
}),
})
}
/// The logs refuse writes, and the journal fully syncs every record (the shutdown's `flush`
/// then checkpoints).
/// No write lands after this returns; then the shutdown's last checkpoint, while the journal is
/// still open; then the journal closes, every record fully synced. The last checkpoint runs
/// here and not after (delta review of #35866, D1): a log sync that fails in it, or in a sync
/// under way that it waits for, freezes the store, and its `frozen` record must reach the
/// journal. A checkpoint after this (`Hub::seal`'s flush) finds no work.
fn seal(&self) {
// Under the appends' lock: an append lands in the log and the journal, or in neither.
let _order = self.inner.order.lock_or_recover();
Store::seal(&self.inner.local);
{
// Under the appends' lock: an append lands in the log and the journal, or in neither.
let _order = self.inner.order.lock_or_recover();
Store::seal(&self.inner.local);
}
if let Err(error) = self.inner.checkpoint() {
tracing::warn!("the event journal's last checkpoint: {error:#}");
}
let order = self.inner.order.lock_or_recover();
if let Err(error) = self.inner.journal.seal(SEAL_WAIT) {
tracing::error!("seal the event journal: {error}");
// Fail closed: a frozen store's record may not be durable.
let frozen = order.frozen;
drop(order);
if frozen {
self.inner.keep_frozen_marker(
"the event journal did not close with its frozen record durable",
);
}
}
}
fn close_log(&self, id: &str) {
Expand All @@ -668,17 +768,28 @@ impl Store for JournalStore {
fn stage_compaction(&self, id: &str, mode: CompactMode) -> Result<Option<Staged>> {
Store::stage_compaction(&self.inner.local, id, mode)
}
/// A checkpoint first, so no journal record of the chat is past it: a replay never writes
/// the lines a retention rewrite cut back into its log. A frozen or failed checkpoint stops
/// the rewrite. The commit (its record before and after the renames, the renames, the folder's
/// sync after them) runs in `counted_sync`, so a failed sync in it freezes the store, as a
/// failed log sync does (#35905): a later sync of the folder may report success for the
/// No journal record of the chat may be past the checkpoint: a replay must never write the
/// lines a retention rewrite cut back into its log. A chat whose newest log record is at or
/// below it (`Order::log_lsn`; retention takes chats untouched for days) needs no checkpoint;
/// any other runs one first, and a frozen or failed checkpoint stops the rewrite. The caller
/// holds the chat's append lock (no record of it lands meanwhile), so a checkpoint in flight
/// (the one after a restart's replay takes seconds with many chats) never holds that lock for
/// an old chat. The commit (its record before and after the renames, the renames, the
/// folder's sync after them) runs in `counted_sync`, so a failed sync in it freezes the store,
/// as a failed log sync does (#35905): a later sync of the folder may report success for the
/// renames a failed one dropped, so no checkpoint may go past it. The rewritten copies are
/// synced earlier, in `stage_compaction`, before any rename: a failure there only fails the
/// pass and changes no log.
fn commit_compaction(&self, id: &str, staged: Staged) -> Result<bool> {
let inner = &self.inner;
inner.checkpoint()?;
let covered = {
let order = inner.order.lock_or_recover();
!order.frozen
&& order.log_lsn.get(id).is_none_or(|newest| *newest <= order.checkpointed)
};
if !covered {
inner.checkpoint()?;
}
inner.counted_sync("a rewritten event log failed to sync", || {
Store::commit_compaction(&inner.local, id, staged)
})
Expand Down Expand Up @@ -714,6 +825,16 @@ impl Store for JournalStore {
}
Store::delete(&inner.local, id)?;
}
// Its folder is gone: the segments the open wrote back need no sync. A handle the open
// kept for one is closed here, off the appends' lock (a close can start disk work).
let dir = inner.local.dir(id);
let gone: Vec<repair::WrittenBack> = {
let mut order = inner.order.lock_or_recover();
let paths: Vec<PathBuf> =
order.read_segments.keys().filter(|path| path.starts_with(&dir)).cloned().collect();
paths.iter().filter_map(|path| order.read_segments.remove(path)).collect()
};
drop(gone);
if let Some(path) = repair {
inner.remove_repair(&path)?;
inner.order.lock_or_recover().repairs.remove(id);
Expand Down
Loading
Loading