fix(causal): born current — slot reuse booked phantom park forgiveness

A fresh/reused slot started with causal_delay=0 and causal_parked=true:
the first resume then 'forgave' the entire monotone global backlog, once
per spawn, booked as park forgiveness. Under close-mode conn churn (~95k
spawns/s) that is millions of phantom forgiven ms per 700ms window, even
in 0% cells — the books could not balance under actor churn while
ka-mode stayed plausible (few spawns). Impacts were unaffected: the
first resume always precedes the first check, so nobody ever spun.

reset_counters now installs Coz's new-thread rule: causal_delay starts
at the current global ledger and causal_parked starts false — a newborn
neither owes nor is forgiven the process's history, and delay injected
while it sits spawn-queued (runnable, not blocked) is owed and paid at
its first check, the semantics audit_zero_pct_window_absorbs_leftover_
debt pins. That test (born failing, unmasked by the run() propagation
fix) and the new actor_churn_between_experiments_forgives_nothing
regression test both go green.
This commit is contained in:
Claude (sandbox)
2026-07-18 21:55:56 +00:00
parent 7eae56a296
commit d5a3ba1934
2 changed files with 72 additions and 6 deletions
+22 -6
View File
@@ -463,8 +463,10 @@ pub(crate) struct Slot {
/// instant-wake re-queue). Consumed by `causal::on_resume`: only a wake
/// from genuine blocking forgives outstanding virtual delay; a merely
/// preempted (runnable) actor stays in debt and must pay by spinning.
/// Starts true so a fresh spawn doesn't inherit the process's whole delay
/// history. Written by the owning scheduler thread at the deschedule /
/// Starts false: a fresh spawn is born current (`reset_counters` sets
/// `causal_delay` to the global ledger), and anything injected while it
/// sits spawn-queued is owed, not waived. Written by the owning
/// scheduler thread at the deschedule /
/// resume boundary only — same single-writer discipline as `causal_delay`.
causal_parked: AtomicBool,
/// RFC 007 — TSC at this actor's last *runnable* (yield) deschedule
@@ -495,7 +497,10 @@ impl Slot {
budget_cycles: AtomicU64::new(0),
causal_site: AtomicU32::new(0),
causal_delay: AtomicU64::new(0),
causal_parked: AtomicBool::new(true),
// Overwritten at every occupancy by `reset_counters` (born
// current); false so a hypothetical reset-skipping path cannot
// waive the whole global backlog at first resume.
causal_parked: AtomicBool::new(false),
causal_desched_tsc: AtomicU64::new(0),
causal_desched_epoch: AtomicU64::new(0),
cold: RawMutex::new(SlotCold {
@@ -587,10 +592,21 @@ impl Slot {
self.messages_received.store(0, Ordering::Relaxed);
self.budget_cycles.store(0, Ordering::Relaxed);
self.causal_site.store(0, Ordering::Relaxed);
// Born current (Coz's new-thread rule): a fresh incarnation neither
// owes the process's accumulated delay history nor books it as park
// forgiveness. The old init (delay 0, parked true) waived the whole
// monotone backlog once per spawn via the first resume — found live
// under close-mode conn churn (~95k spawns/s): millions of phantom
// forgiven ms per 700ms window, even in 0% cells. Parked starts
// false: delay injected while spawn-queued is *owed* (the newborn is
// runnable, not blocked) and paid at its first check — the semantics
// `audit_zero_pct_window_absorbs_leftover_debt` pins.
#[cfg(feature = "smarm-causal")]
self.causal_delay
.store(crate::causal::global_delay_cycles(), Ordering::Relaxed);
#[cfg(not(feature = "smarm-causal"))]
self.causal_delay.store(0, Ordering::Relaxed);
// True, not false: the first resume of a fresh actor is a "wake" —
// it must not owe the process's accumulated delay history.
self.causal_parked.store(true, Ordering::Relaxed);
self.causal_parked.store(false, Ordering::Relaxed);
self.causal_desched_tsc.store(0, Ordering::Relaxed);
self.causal_desched_epoch.store(0, Ordering::Relaxed);
}