Put the elaboration call site and the quarantine stop under test (L1 Cut 4)
- Dungeon: the body of elaborate_world's loop becomes run_elaboration_sweep(&state). It checks quarantine, runs the sweep on state.elaboration_permits, and logs any error at debug, exactly as before. The interval loop is unchanged. - saturating_one_pool_never_delays_the_other now runs its second half through run_elaboration_sweep. The production call site is therefore the one tested: passing controller_permits there fails the test, which was not true when the test called sweep directly. - Library: no_session_starts_after_a_quarantine_error pins the sweep's stop rule. The pool holds one permit and the first session faults with an integrity violation, so the second entry never reaches the provider. This commit builds. Library 457 + 1 ignored, Dungeon 53 + 1 ignored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b3963f911d
commit
bc31d1196f
2 changed files with 51 additions and 22 deletions
|
|
@ -1829,26 +1829,31 @@ const ELABORATION_SWEEP_INTERVAL: Duration = Duration::from_secs(300);
|
|||
|
||||
/// The authoring lane's only driver. It runs when the cognition organ opened at
|
||||
/// all, which is the config gate the runtime already has: no mode flag joins it.
|
||||
/// Each sweep runs its sessions under the elaboration pool and never touches the
|
||||
/// simulation pool. Sweeps never overlap, because this loop awaits one before it
|
||||
/// ticks the next, so one sweep's claim set is the only set of sessions alive.
|
||||
/// Sweeps never overlap, because this loop awaits one before it ticks the next,
|
||||
/// so one sweep's claim set is the only set of sessions alive.
|
||||
async fn elaborate_world(state: AppState) {
|
||||
let mut interval = tokio::time::interval(ELABORATION_SWEEP_INTERVAL);
|
||||
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
interval.tick().await;
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let Some(runner) = state
|
||||
.controllers
|
||||
.as_deref()
|
||||
.filter(|_| !state.controller_quarantined.load(Ordering::SeqCst))
|
||||
.map(ControllerRunner::elaborator)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if let Err(error) = runner.sweep(state.elaboration_permits.clone()).await {
|
||||
tracing::debug!(%error, "elaboration sweep did not complete");
|
||||
}
|
||||
run_elaboration_sweep(&state).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// One sweep, apart from the wall clock that decides when to run it. Its
|
||||
/// sessions run under the elaboration pool and never touch the simulation pool.
|
||||
async fn run_elaboration_sweep(state: &AppState) {
|
||||
let Some(runner) = state
|
||||
.controllers
|
||||
.as_deref()
|
||||
.filter(|_| !state.controller_quarantined.load(Ordering::SeqCst))
|
||||
.map(ControllerRunner::elaborator)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if let Err(error) = runner.sweep(state.elaboration_permits.clone()).await {
|
||||
tracing::debug!(%error, "elaboration sweep did not complete");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3398,14 +3403,9 @@ mod tests {
|
|||
.clone()
|
||||
.try_acquire_many_owned(u32::try_from(TEST_CONTROLLER_CONCURRENCY).expect("a small pool"))
|
||||
.expect("the simulation pool is free");
|
||||
let runner = state.controllers.clone().expect("the runner");
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(30),
|
||||
runner.elaborator().sweep(state.elaboration_permits.clone()),
|
||||
)
|
||||
.await
|
||||
.expect("the sweep waited on the simulation pool")
|
||||
.expect("the sweep completed");
|
||||
tokio::time::timeout(Duration::from_secs(30), run_elaboration_sweep(&state))
|
||||
.await
|
||||
.expect("the sweep waited on the simulation pool");
|
||||
assert!(
|
||||
port.calls.load(Ordering::SeqCst) > before,
|
||||
"no elaboration inference completed"
|
||||
|
|
|
|||
|
|
@ -11291,6 +11291,35 @@ mod tests {
|
|||
task.await.unwrap();
|
||||
}
|
||||
|
||||
/// No session starts after one has returned a quarantine-class error. Under
|
||||
/// a pool of one the second entry waits for the first session's permit; the
|
||||
/// first session faults with an integrity violation, and the second never
|
||||
/// reaches the provider boundary.
|
||||
#[tokio::test]
|
||||
async fn no_session_starts_after_a_quarantine_error() {
|
||||
let (_directory, mailbox, task, _commons, _roads) = dead_end_world(
|
||||
crate::tests::stock_weights(),
|
||||
&["The Unwalked Road", "The Far Road"],
|
||||
Roots::Commons,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
let port = Arc::new(SweepPort::new(|_, _| {
|
||||
Err(InferenceFault::integrity_violation(
|
||||
"the fixture port disputes every receipt",
|
||||
))
|
||||
}));
|
||||
let runner = sweep_runner(&mailbox, port.clone(), fresh_store());
|
||||
let Err(error) = runner.sweep(pool(1)).await else {
|
||||
panic!("a quarantine-class fault did not end the sweep with an error");
|
||||
};
|
||||
assert!(error.requires_quarantine(), "{error:?}");
|
||||
assert_eq!(port.calls(), 1, "a session started after a quarantine-class error");
|
||||
drop(runner);
|
||||
drop(mailbox);
|
||||
task.await.unwrap();
|
||||
}
|
||||
|
||||
/// Probe 3's deficit half, through the sweep. Sweep one's deficit session
|
||||
/// is refused; a clock tick moves the ancestry, and with it the id a fresh
|
||||
/// derivation would give the deficit. Sweep two finds the refused session by
|
||||
|
|
|
|||
Loading…
Reference in a new issue