Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 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
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
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
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
7 changes: 4 additions & 3 deletions cmux-tui/crates/acpmux/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1482,9 +1482,10 @@ When no config exists, harnesses are imported from `~/.acpx/config.json` (its `a
could not write into a log that changed without the journal (a run with the switch off after a
crash, or an older build) go to `journal-unapplied/`. Turning it off is safe: the next start
writes the journal's lines into the logs first. A failed log sync stops the daemon for a restart,
which writes the logs again. With it on, an event line over 64 MiB (the journal's record limit)
is refused, and its log does not get it either; with it off, the log takes it. Read when the
daemon starts.
which writes the logs again: it exits with status 74, and its supervisor (or the next client)
starts it again. A reply and an acknowledged event wait until what they show is durable. With it
on, an event line over 64 MiB (the journal's record limit) is refused, and its log does not get
it either; with it off, the log takes it. Read when the daemon starts.
- `websocket`: optional. The same protocol over WebSocket text frames, for remote clients such
as a dashboard or a Durable Object. Put it behind a tunnel; the token is a bearer token.

Expand Down
10 changes: 7 additions & 3 deletions cmux-tui/crates/acpmux/src/cli/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,10 @@ async fn async_main(args: Vec<OsString>, invocation: Invocation) -> Result<()> {
)
.with_target(false)
.init();
crate::qos::enter(crate::qos::Role::Daemon);
// With `store.journal` on, an acknowledged reply waits on the journal's sync, which a
// utility disk tier throttles 15-30x under another app's disk work (STATUS 11554):
// the daemon stays at the default tier, and only its background threads yield.
crate::qos::enter_daemon(crate::config::store_journal_enabled());
let result = crate::daemon::run(DaemonOptions {
ws_listen: listen,
ws_token: token,
Expand Down Expand Up @@ -263,8 +266,9 @@ async fn async_main(args: Vec<OsString>, invocation: Invocation) -> Result<()> {
}
// The daemon has stopped its agents and synced its store. Exit
// now rather than wait for the runtime to drain blocking tasks
// (a launcher `--version` check can take 20 s).
std::process::exit(0)
// (a launcher `--version` check can take 20 s); 74 when its store
// asked for a restart (`daemon::EXIT_REPAIR`).
std::process::exit(crate::daemon::exit_status())
}
Some(Command::CliAgent {
last_block,
Expand Down
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 @@ -2050,7 +2050,7 @@ pub use discover::{
mod peer;
pub use peer::PeerConfig;
mod store_config;
pub use store_config::{StoreConfig, StoreMode};
pub use store_config::{StoreConfig, StoreMode, store_journal_enabled};
mod pool;
pub use pool::PoolConfig;
mod btw;
Expand Down
24 changes: 24 additions & 0 deletions cmux-tui/crates/acpmux/src/config/store_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,27 @@ impl Default for StoreConfig {
}
}
}

/// Whether `store.journal` is on in this home's config file, read without loading the rest of the
/// config (no harness discovery): the daemon's process tier is set before it starts any thread
/// (`qos::enter_daemon`). A file that is missing or does not read counts as off, the default; the
/// daemon's own config load then reports the error.
pub fn store_journal_enabled() -> bool {
#[derive(Deserialize)]
struct File {
#[serde(default)]
store: Option<Store>,
}
#[derive(Deserialize)]
struct Store {
#[serde(default)]
journal: bool,
}
let path = super::Config::path();
let Ok(mut file) = super::private_state_file(&path) else { return false };
let mut text = String::new();
if std::io::Read::read_to_string(&mut file, &mut text).is_err() {
return false;
}
serde_json::from_str::<File>(&text).ok().and_then(|file| file.store).is_some_and(|s| s.journal)
}
11 changes: 4 additions & 7 deletions cmux-tui/crates/acpmux/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ mod stop_causes;
#[cfg(unix)]
use stop_causes::next_signal;
use stop_causes::{AbortOnDrop, store_asks_to_stop};
pub use stop_causes::{EXIT_REPAIR, exit_status};
#[cfg(unix)]
pub(crate) mod term_sender;
pub(crate) mod unused_exit;
Expand Down Expand Up @@ -719,7 +720,7 @@ pub async fn run(opts: DaemonOptions) -> Result<()> {
let tailnet = crate::tailnet::embedded_service::Service::spawn(hub.clone());
// The store asks for a restart when it cannot go on safely (`Store::stop_reason`): a frozen
// event journal checkpoint, whose logs only the next start's replay writes again.
let mut store_stop = std::pin::pin!(store_asks_to_stop(hub.store.stop_reason()));
let mut store_stop = std::pin::pin!(store_asks_to_stop(hub.clone()));
let shutdown = async {
// Every stop names its cause in the log (`app_orphan` and `unused` name their own).
#[cfg(unix)]
Expand All @@ -743,9 +744,7 @@ pub async fn run(opts: DaemonOptions) -> Result<()> {
_ = next_signal(int.as_mut()) => tracing::warn!("stopping: SIGINT"),
_ = hub.shutdown.notified() => tracing::warn!("stopping: a client asked it to"),
// A restart handoff: the next start repairs what the store could not.
() = &mut store_stop => {
hub.restart_requested.store(true, std::sync::atomic::Ordering::SeqCst);
},
() = &mut store_stop => {},
// No build of its app is left to adopt its agents, so they end with it. An app
// that only quit or was updated keeps them, as its Quit does
// (`daemon/app_orphan.rs`).
Expand Down Expand Up @@ -799,9 +798,7 @@ pub async fn run(opts: DaemonOptions) -> Result<()> {
tokio::select! {
_ = ctrl_c => {},
_ = hub.shutdown.notified() => {},
() = &mut store_stop => {
hub.restart_requested.store(true, std::sync::atomic::Ordering::SeqCst);
},
() = &mut store_stop => {},
// No build of its app is left to adopt its agents, so they end with it. An app
// that only quit or was updated keeps them, as its Quit does
// (`daemon/app_orphan.rs`).
Expand Down
33 changes: 26 additions & 7 deletions cmux-tui/crates/acpmux/src/daemon/stop_causes.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
//! What stops the daemon besides a client: a signal, and the store asking for a restart (moved
//! out of `daemon.rs`, which is over its size budget).

use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

/// The daemon's exit status after its store asked it to stop for a restart ([`store_asks_to_stop`]:
/// a failed log sync froze the event journal's checkpoint, or the journal's writer ended): EX_IOERR.
/// The next start's replay repairs the logs; a supervisor starts it again (launchd `KeepAlive`,
/// systemd `Restart=always`: the units prevent only 75), and otherwise the next client does.
pub const EXIT_REPAIR: i32 = 74;

/// The store's stop won the daemon's stop select.
static STOPPED_FOR_REPAIR: AtomicBool = AtomicBool::new(false);

/// The process's exit status after `daemon::run` returned: [`EXIT_REPAIR`] when the store asked
/// for the stop, else 0.
pub fn exit_status() -> i32 {
if STOPPED_FOR_REPAIR.load(Ordering::SeqCst) { EXIT_REPAIR } else { 0 }
}

/// Aborts a task when dropped.
pub(super) struct AbortOnDrop(pub(super) tokio::task::AbortHandle);

Expand All @@ -10,18 +28,19 @@ impl Drop for AbortOnDrop {
}
}

/// Resolves, with the reason logged, once the store asks the daemon to stop
/// (`Store::stop_reason`); never for a store that does not ask, or once the store is gone.
pub(super) async fn store_asks_to_stop(
reason: Option<tokio::sync::watch::Receiver<Option<String>>>,
) {
let Some(mut reason) = reason else { return std::future::pending().await };
/// Resolves once the store asks the daemon to stop (`Store::stop_reason`), with the reason
/// logged, the stop marked a restart handoff (WebSocket clients get 1012) and the exit status set
/// to [`EXIT_REPAIR`]; never for a store that does not ask, or once the store is gone.
pub(super) async fn store_asks_to_stop(hub: Arc<crate::hub::Hub>) {
let Some(mut reason) = hub.store.stop_reason() else { return std::future::pending().await };
match reason.wait_for(Option::is_some).await {
Ok(why) => {
tracing::error!("stopping for a restart: {}", why.as_deref().unwrap_or_default());
}
Err(_) => std::future::pending().await,
Err(_) => return std::future::pending().await,
}
hub.restart_requested.store(true, Ordering::SeqCst);
STOPPED_FOR_REPAIR.store(true, Ordering::SeqCst);
}

/// The next delivery of a registered signal; never, if registration failed.
Expand Down
12 changes: 10 additions & 2 deletions cmux-tui/crates/acpmux/src/hub/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2327,14 +2327,22 @@ impl Hub {
self.store.events(id, after, limit)
}

/// Visits the records past `after` in order, one at a time, until `visit` returns false.
/// Visits the records past `after` in order, one at a time, until `visit` returns false. A
/// caller sends each record as it reads it (`session/load`'s replay), so the visit stops at
/// the chat's `last_seq` read first: a line the log holds before its journal record is
/// appended (R-DURABILITY S2c) is never sent ahead of that record. A chat no session holds
/// takes no appends: read to the end.
pub fn scan_events(
&self,
id: &str,
after: u64,
visit: &mut dyn FnMut(EventRecord) -> bool,
) -> Result<()> {
self.store.scan(id, after, visit)
let live = self.sessions.lock().unwrap_or_else(|e| e.into_inner()).get(id).cloned();
let Some(last) = live.map(|session| session.meta().last_seq) else {
return self.store.scan(id, after, visit);
};
self.store.scan(id, after, &mut |record| record.seq <= last && visit(record))
}

pub fn session_dir(&self, id: &str) -> Option<PathBuf> {
Expand Down
38 changes: 37 additions & 1 deletion cmux-tui/crates/acpmux/src/qos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,28 @@ fn agent_background() -> bool {
std::env::var_os(ENV).is_some_and(|v| v == "background")
}

/// The daemon's tier once its config is read: with `store.journal` on, acknowledged replies wait
/// on the journal's sync, so the daemon keeps the default disk tier (lead rule 2026-11-04: a reply
/// the person waits for never waits on a throttled sync) and only its background threads yield
/// ([`background_thread`]). Without the journal, [`Tier::Serving`] as before.
pub fn enter_daemon(journal: bool) {
if journal {
// The default disk tier: nothing to set (the CPU is unchanged for the daemon either way).
return;
}
enter(Role::Daemon);
}

/// The calling thread, dedicated to background work for its whole life (the event journal's
/// checkpointer): its disk I/O at the utility tier. Only for a thread that does nothing else: a
/// thread-scope I/O policy also drops the thread out of QoS for good (see `imp::ThreadOnScreen`),
/// so a pool thread that is reused must never take it.
pub fn background_thread() {
if enabled() {
imp::thread_disk_utility();
}
}

/// Put this process in `role`'s tier. Call it first thing in `main`, before
/// any thread starts: for [`Tier::Agent`] on macOS it re-executes the process
/// in place and returns only when that is not done (already clamped, turned
Expand Down Expand Up @@ -158,7 +180,6 @@ mod imp {

const IOPOL_TYPE_DISK: libc::c_int = 0;
const IOPOL_SCOPE_PROCESS: libc::c_int = 0;
#[cfg(test)]
const IOPOL_SCOPE_THREAD: libc::c_int = 1;
const IOPOL_UTILITY: libc::c_int = 4;
const PRIO_DARWIN_BG: libc::c_int = 0x1000;
Expand Down Expand Up @@ -187,6 +208,14 @@ mod imp {
}
}

/// The calling thread's disk I/O at utility (`background_thread`).
pub(super) fn thread_disk_utility() {
// SAFETY: plain syscall wrapper on the calling thread with constant arguments.
if unsafe { setiopolicy_np(IOPOL_TYPE_DISK, IOPOL_SCOPE_THREAD, IOPOL_UTILITY) } != 0 {
tracing::debug!("setiopolicy_np (thread): {}", std::io::Error::last_os_error());
}
}

/// The clamp setter, looked up at run time: it is not in the public SDK
/// headers, so a macOS without it starts unclamped instead of failing to load.
fn clamp_fn() -> Option<ClampFn> {
Expand Down Expand Up @@ -362,6 +391,7 @@ mod imp {
#[cfg(target_os = "linux")]
mod imp {
pub(super) fn disk_utility() {}
pub(super) fn thread_disk_utility() {}

/// nice +5: inherited by everything the process starts. Not undone (an
/// unprivileged process cannot lower its nice value), and not needed to be.
Expand Down Expand Up @@ -397,6 +427,7 @@ mod imp {
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
mod imp {
pub(super) fn disk_utility() {}
pub(super) fn thread_disk_utility() {}
pub(super) fn clamp_cpu_utility() {}
pub(super) fn process_background() {
clamp_cpu_utility();
Expand All @@ -417,6 +448,11 @@ mod imp {
}
}

// Its tests read macOS disk policies only.
#[cfg(all(test, target_os = "macos"))]
#[path = "qos_journal_tests.rs"]
mod journal_tests;

#[cfg(test)]
mod tests {
use super::*;
Expand Down
79 changes: 79 additions & 0 deletions cmux-tui/crates/acpmux/src/qos_journal_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//! The daemon's disk tier with the event journal on (R-DURABILITY S2c; lead rule 2026-11-04: a
//! reply the person waits for never waits on a throttled disk sync). Fleet A/B (STATUS 11554):
//! at `IOPOL_UTILITY` an acknowledged append waited about 16 ms p50 under another process's disk
//! stress, against 0.7 to 1.1 ms at the default tier.

use super::*;

/// The process's disk I/O policy (macOS `getiopolicy_np`, process scope).
#[cfg(target_os = "macos")]
fn process_io() -> libc::c_int {
unsafe extern "C" {
fn getiopolicy_np(iotype: libc::c_int, scope: libc::c_int) -> libc::c_int;
}
// SAFETY: a plain syscall with constant arguments.
unsafe { getiopolicy_np(0, 0) }
}

/// The calling thread's own disk I/O policy (0: it follows the process's).
#[cfg(target_os = "macos")]
fn thread_io() -> libc::c_int {
unsafe extern "C" {
fn getiopolicy_np(iotype: libc::c_int, scope: libc::c_int) -> libc::c_int;
}
// SAFETY: as above, thread scope.
unsafe { getiopolicy_np(0, 1) }
}

/// With the journal on, the daemon's process keeps the default disk tier; without it, utility as
/// before. A test process's own policy, so each case runs in a child of this test binary.
#[cfg(target_os = "macos")]
#[test]
fn the_daemon_with_the_journal_keeps_the_default_disk_tier() {
if let Ok(case) = std::env::var("ACPMUX_QOS_JOURNAL_CASE") {
enter_daemon(case == "journal");
println!("QOS-CASE io={}", process_io());
return;
}
let exe = std::env::current_exe().unwrap();
let mut seen = Vec::new();
for case in ["journal", "nojournal"] {
let out = std::process::Command::new(&exe)
.args([
"--exact",
"qos::journal_tests::the_daemon_with_the_journal_keeps_the_default_disk_tier",
"--nocapture",
])
.env("ACPMUX_QOS_JOURNAL_CASE", case)
.env_remove(ENV)
.output()
.unwrap();
let text = String::from_utf8_lossy(&out.stdout).into_owned();
let io = text
.lines()
.find_map(|line| line.strip_prefix("QOS-CASE io="))
.and_then(|io| io.trim().parse::<i32>().ok())
.unwrap_or_else(|| panic!("case {case}: no QOS-CASE line in {text}"));
seen.push((case, io));
}
// 4 is IOPOL_UTILITY; the default reads 0 (or 1, IOPOL_IMPORTANT, the same tier).
assert!(matches!(seen[0], ("journal", 0 | 1)), "the daemon with the journal: {seen:?}");
assert_eq!(seen[1], ("nojournal", 4), "the daemon without the journal: {seen:?}");
}

/// A dedicated background thread (the checkpointer) does its disk work at the utility tier, and
/// a thread beside it keeps the process's.
#[cfg(target_os = "macos")]
#[test]
fn a_background_thread_yields_its_disk_work() {
let (own, other) = std::thread::scope(|scope| {
let own = scope.spawn(|| {
background_thread();
thread_io()
});
let other = scope.spawn(thread_io);
(own.join().unwrap(), other.join().unwrap())
});
assert_eq!(own, 4, "the background thread's disk tier");
assert_eq!(other, 0, "a thread beside it");
}
Loading
Loading