Merge B2-3d: the route actuation ceiling counts only Forward

Operator ruling 3d A. Survival actuations are recorded, never counted or
refused. Soul: verify.sh clean at 057bdd5 (341); own mutants killed. Known,
not new: Survival retries inside one transaction reload nginx unbounded
(~120/h per target) until B5, which is the next Idunn cut. Operator
approved installing B2 ahead of B5 (2026-09-30).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 15:11:18 +02:00
commit a96ad9d4e6
2 changed files with 182 additions and 64 deletions

View file

@ -73,9 +73,17 @@ const CONTINUITY_RESTART_BACKOFF_MILLIS: u64 = 5_000;
/// Forward route actuations (fragment write, firewall, `nginx -t`, reload,
/// private-mount validation) one target may perform per sliding window. A
/// healthy target performs none; a legitimate deployment performs one or two.
/// Survival actuations are counted against it but never refused.
/// Only deployments count: a Survival actuation is recorded beside this
/// ceiling, never counted against it and never refused by it, so a target
/// whose continuity spent its restarts can still be rescued by a deploy. Each
/// Survival path has its own bound instead: a continuity's actuations the
/// restart log, an admitted-route repair the challenge backoff, and a
/// deployment's rollback or withdrawal the Forward change it undoes.
const ROUTE_ACTUATION_CEILING: usize = 12;
const ROUTE_ACTUATION_WINDOW_MILLIS: u64 = 3_600_000;
/// How many Survival route actuation times a target keeps, for `status`
/// only: they are a record, not a meter, and no decision reads them.
const ROUTE_SURVIVAL_RECORD: usize = 12;
/// The longest wait between route challenges while proofs keep failing. The
/// shortest is the observation max age.
const ROUTE_CHALLENGE_BACKOFF_CAP_MILLIS: u64 = 600_000;
@ -934,8 +942,9 @@ impl RouteSupervisionState {
}
/// What one target has been allowed to do, kept across every generation and
/// present before the first: route actuations and continuity restarts, each a
/// sliding log of the times they happened. Owned by the meter primitives below;
/// present before the first: Forward route actuations and continuity restarts,
/// each a sliding log of the times they happened, and a record of Survival
/// route actuations. Owned by the meter primitives below;
/// the route driver only asks, and no generation write carries it.
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
@ -947,7 +956,8 @@ struct TargetSupervision {
schema_version: String,
#[cultcache(key = 1)]
target: String,
/// The newest `ROUTE_ACTUATION_CEILING` actuation times, ascending.
/// The newest `ROUTE_ACTUATION_CEILING` Forward actuation times,
/// ascending: the only entries the ceiling counts.
#[cultcache(key = 2)]
route_actuations: Vec<u64>,
/// The newest `CONTINUITY_RESTART_ATTEMPTS` restart times, ascending.
@ -958,6 +968,10 @@ struct TargetSupervision {
continuity_deferred_until: Option<u64>,
#[cultcache(key = 5)]
continuity_deferral_reason: Option<String>,
/// The newest `ROUTE_SURVIVAL_RECORD` Survival actuation times, ascending.
/// Visibility only: never counted, never refused.
#[cultcache(key = 6)]
route_survivals: Vec<u64>,
}
/// The entries of a sliding log still inside `window` at `now`. An entry from
@ -970,8 +984,8 @@ fn live_entries(log: &[u64], now: u64, window: u64) -> Vec<u64> {
.collect()
}
/// Record `now`, keeping the newest `limit` entries. That is exact for "at most
/// `limit` in any window", even after a forced entry pushes past the limit.
/// Record `now`, keeping the newest `limit` entries: all a meter needs to
/// decide "at most `limit` in any window".
fn record_entry(log: &mut Vec<u64>, now: u64, limit: usize) {
clamp_to(log, now);
log.push(now);
@ -999,14 +1013,16 @@ impl TargetSupervision {
continuity_restarts: Vec::new(),
continuity_deferred_until: None,
continuity_deferral_reason: None,
route_survivals: Vec::new(),
}
}
/// Clamp both logs to `now`. Callers that decide from a log write the
/// Clamp every log to `now`. Callers that decide from a log write the
/// result back when it changed.
fn settle(&mut self, now: u64) {
clamp_to(&mut self.route_actuations, now);
clamp_to(&mut self.continuity_restarts, now);
clamp_to(&mut self.route_survivals, now);
}
fn route_used(&self, now: u64) -> usize {
@ -1020,23 +1036,33 @@ impl TargetSupervision {
.then(|| live[live.len() - ROUTE_ACTUATION_CEILING] + ROUTE_ACTUATION_WINDOW_MILLIS)
}
/// Count one route actuation. Only a `Forward` change can be refused; a
/// `Survival` is always admitted, and counted whenever the ledger can record it.
/// Charge one route actuation. A `Forward` change is counted against the
/// ceiling and refused at it; a `Survival` is only recorded, never
/// counted and never refused.
fn charge_route(&mut self, now: u64, kind: RouteActuation) -> Result<(), RouteActuationRefused> {
self.settle(now);
if kind == RouteActuation::Forward
&& let Some(reopens_at) = self.route_reopens_at(now)
{
return Err(RouteActuationRefused {
target: self.target.clone(),
used: self.route_used(now),
reopens_at_unix_millis: reopens_at,
});
match kind {
RouteActuation::Forward => {
if let Some(reopens_at) = self.route_reopens_at(now) {
return Err(RouteActuationRefused {
target: self.target.clone(),
used: self.route_used(now),
reopens_at_unix_millis: reopens_at,
});
}
record_entry(&mut self.route_actuations, now, ROUTE_ACTUATION_CEILING);
}
RouteActuation::Survival => {
record_entry(&mut self.route_survivals, now, ROUTE_SURVIVAL_RECORD);
}
}
record_entry(&mut self.route_actuations, now, ROUTE_ACTUATION_CEILING);
Ok(())
}
fn survivals_in_window(&self, now: u64) -> usize {
live_entries(&self.route_survivals, now, ROUTE_ACTUATION_WINDOW_MILLIS).len()
}
fn restarts_used(&self, now: u64) -> usize {
live_entries(&self.continuity_restarts, now, CONTINUITY_RESTART_WINDOW_MILLIS).len()
}
@ -1091,6 +1117,7 @@ impl TargetSupervision {
for (log, limit, what) in [
(&self.route_actuations, ROUTE_ACTUATION_CEILING, "route actuation"),
(&self.continuity_restarts, CONTINUITY_RESTART_ATTEMPTS, "continuity restart"),
(&self.route_survivals, ROUTE_SURVIVAL_RECORD, "survival route actuation"),
] {
ensure!(log.len() <= limit, "{what} log exceeds its bound");
ensure!(
@ -4226,6 +4253,7 @@ fn render_supervision(
}
if generation.is_none_or(|value| value.route_supervision.is_some())
|| !meters.route_actuations.is_empty()
|| !meters.route_survivals.is_empty()
{
lines.push(format!(
" route actuations {}/{} in window, reopens-at {}",
@ -4233,6 +4261,15 @@ fn render_supervision(
ROUTE_ACTUATION_CEILING,
at(meters.route_reopens_at(now))
));
// A record, not a meter: only the newest are kept, so a saturated
// count says "at least".
let survivals = meters.survivals_in_window(now);
lines.push(format!(
" route survival actuations {}{} in window (uncounted), last-at {}",
if survivals == ROUTE_SURVIVAL_RECORD { "at least " } else { "" },
survivals,
at(meters.route_survivals.last().copied())
));
}
lines
}
@ -4368,7 +4405,7 @@ struct Engine {
}
/// The route driver's door to the actuation ceiling: every group of host
/// mutations a driver makes for `target` is counted through here. The gate
/// mutations a driver makes for `target` is charged through here. The gate
/// owns which charge a request is: a driver asks for the change it is making,
/// and the command that owns the transaction decides whether that change is
/// deployment (`Forward`, refusable) or the continuity of the admitted
@ -4475,20 +4512,20 @@ impl Engine {
}
}
/// Count one route actuation against the target's sliding ceiling, in its
/// `TargetSupervision`, before the driver acts. The record is created by
/// the first charge, so a first deployment is metered like any other. A
/// refused `Forward` is the typed `RouteActuationRefused`, and a `Forward`
/// that cannot be recorded is refused too: no charge, no actuation. A
/// `Survival` is never refused, so it is counted when it can be and runs
/// regardless when it cannot: an unwritable store must not stop a rollback,
/// a withdrawal or a repair.
/// Charge one route actuation to the target's `TargetSupervision` before
/// the driver acts. The record is created by the first charge, so a first
/// deployment is metered like any other. A `Forward` is counted against
/// the sliding ceiling; a refused one is the typed `RouteActuationRefused`,
/// and one that cannot be recorded is refused too: no charge, no
/// actuation. A `Survival` is never counted or refused, only recorded when
/// it can be, and runs regardless when it cannot: an unwritable store must
/// not stop a rollback, a withdrawal or a repair.
fn charge_route_actuation(&self, target: &str, kind: RouteActuation) -> Result<()> {
match self.record_route_actuation(target, kind) {
Ok(charged) => charged.map_err(anyhow::Error::new),
Err(error) if kind == RouteActuation::Survival => {
eprintln!(
"Idunn could not count a survival route actuation of {target}; it proceeds uncounted: {error:#}"
"Idunn could not record a survival route actuation of {target}; it proceeds unrecorded: {error:#}"
);
Ok(())
}
@ -12317,6 +12354,8 @@ mod tests {
meters(|m| m.route_actuations = (1..=13).collect()).contains("exceeds its bound")
);
assert!(meters(|m| m.continuity_restarts = (1..=7).collect()).contains("exceeds its bound"));
assert!(meters(|m| m.route_survivals = (1..=13).collect()).contains("exceeds its bound"));
assert!(meters(|m| m.route_survivals = vec![9, 5]).contains("ascending"));
assert!(meters(|m| m.continuity_deferred_until = Some(5)).contains("time or a reason"));
assert!(meters(|m| m.continuity_deferral_reason = Some("x".into())).contains("time or a reason"));
assert!(meters(|m| m.schema_version = "idunn.target_supervision.v0".into())
@ -15017,22 +15056,32 @@ mod tests {
}
#[test]
fn a_survival_actuation_is_counted_past_the_limit_and_never_refused() -> Result<()> {
fn a_survival_actuation_is_recorded_but_never_counted_or_refused() -> Result<()> {
let t = 1_000_000_000_u64;
let mut meters = TargetSupervision::new("service");
// Survivals first, past the ceiling: every one is admitted and
// recorded, and none takes a Forward slot.
for offset in 0..20 {
meters.charge_route(t + offset, RouteActuation::Survival)?;
}
assert_eq!(meters.route_survivals.len(), ROUTE_SURVIVAL_RECORD);
assert_eq!(meters.route_survivals.last(), Some(&(t + 19)));
assert!(meters.route_actuations.is_empty());
// Twelve Forward changes interleaved with more Survivals: the twelve
// are admitted, and the thirteenth is refused with a reopen time that
// only the oldest Forward entry decides.
for offset in 0..12 {
meters.charge_route(t + offset, RouteActuation::Forward)?;
}
meters.charge_route(t + 20, RouteActuation::Survival)?;
assert_eq!(meters.route_actuations.len(), ROUTE_ACTUATION_CEILING);
assert_eq!(meters.route_actuations.last(), Some(&(t + 20)));
// The survival took a slot: the forward ceiling is still exact, and
// reopens when the oldest surviving entry leaves.
let refused = meters.charge_route(t + 21, RouteActuation::Forward).unwrap_err();
assert_eq!(refused.reopens_at_unix_millis, t + 1 + ROUTE_ACTUATION_WINDOW_MILLIS);
for _ in 0..20 {
meters.charge_route(t + 22, RouteActuation::Survival)?;
meters.charge_route(t + 100 + 2 * offset, RouteActuation::Forward)?;
meters.charge_route(t + 101 + 2 * offset, RouteActuation::Survival)?;
}
let refused = meters.charge_route(t + 200, RouteActuation::Forward).unwrap_err();
assert_eq!(
(refused.used, refused.reopens_at_unix_millis),
(ROUTE_ACTUATION_CEILING, t + 100 + ROUTE_ACTUATION_WINDOW_MILLIS)
);
// A full Forward ceiling still admits and records a Survival.
meters.charge_route(t + 201, RouteActuation::Survival)?;
assert_eq!(meters.route_survivals.last(), Some(&(t + 201)));
meters.validate()?;
Ok(())
}
@ -15260,6 +15309,21 @@ mod tests {
now - 5_000 + ROUTE_ACTUATION_WINDOW_MILLIS
)), "{lines}");
assert!(lines.contains("route healthy consecutive-failures 0"), "{lines}");
assert!(
lines.contains("route survival actuations 0 in window (uncounted), last-at none"),
"{lines}"
);
meters.route_survivals = vec![now - 7_000, now - 3_000];
let lines = render_supervision("service", Some(&generation), Some(&meters), now).join("\n");
assert!(lines.contains(&format!(
"route survival actuations 2 in window (uncounted), last-at {}",
now - 3_000
)), "{lines}");
meters.route_survivals = (0..ROUTE_SURVIVAL_RECORD as u64).map(|n| now - 100 + n).collect();
let lines = render_supervision("service", Some(&generation), Some(&meters), now).join("\n");
assert!(lines.contains("route survival actuations at least 12 in window"), "{lines}");
// Survivals never move the ceiling's count.
assert!(lines.contains("route actuations 12/12 in window"), "{lines}");
// A first deployment has meters and no generation: it shows them alone.
let mut first = TargetSupervision::new("fresh");
@ -15270,6 +15334,13 @@ mod tests {
// An unrouted, unmetered target shows no route line at all.
let lines = render_supervision("plain", Some(&unrouted_generation()?), None, now).join("\n");
assert!(!lines.contains("route actuations"), "{lines}");
// A generation that no longer routes, whose meters hold only survival
// actuations, still shows the record.
let mut survived = TargetSupervision::new("plain");
survived.route_survivals = vec![now - 1];
let lines =
render_supervision("plain", Some(&unrouted_generation()?), Some(&survived), now).join("\n");
assert!(lines.contains("route survival actuations 1 in window"), "{lines}");
Ok(())
}
@ -17380,10 +17451,11 @@ mod tests {
#[test]
fn a_full_ledger_still_restores_the_admitted_and_incumbent_route() -> Result<()> {
let full = ROUTE_ACTUATION_CEILING;
// The newest recorded Survival, or 0; the Forward ledger stays full.
let newest = |world: &EngineFixture| -> Result<u64> {
let meters = meters_of(world, "service")?;
assert_eq!(meters.route_actuations.len(), ROUTE_ACTUATION_CEILING);
Ok(*meters.route_actuations.last().unwrap())
Ok(meters.route_survivals.last().copied().unwrap_or(0))
};
// (i) A post-fence abort withdraws the candidate's membership.
@ -17396,7 +17468,7 @@ mod tests {
.begin_post_fencing_abort(&resident(&aborting.world)?, anyhow!("test"))?;
aborting.step()?;
assert_eq!(aborting.reloads(), 1, "the withdrawal did not run");
assert!(newest(&aborting.world)? > before, "the withdrawal was not counted");
assert!(newest(&aborting.world)? > before, "the withdrawal was not recorded");
// (ii) Supervision repairs a drifted admitted fragment.
let supervised = routed_world(Odin::Unreachable, 1, DeploymentPhase::Fencing, false)?;
@ -17406,11 +17478,12 @@ mod tests {
supervised.drift()?;
assert!(supervised.tick()?);
assert_eq!(supervised.reloads(), reloads + 1, "the repair did not run");
assert!(newest(&supervised.world)? > before, "the repair was not counted");
assert!(newest(&supervised.world)? > before, "the repair was not recorded");
assert!(admitted(&supervised)?.route_supervision.unwrap().degraded_since_unix_millis.is_none());
// (iii) A failed proof at Routing rolls the route back, though the
// install that preceded it took the last free slot.
// install that preceded it took the last free slot. The rollback
// is recorded and leaves the Forward ledger exactly full.
let failing = routed_deploy_world(Odin::Unreachable, 1, DeploymentPhase::Fencing)?;
failing.stub.set_state("active");
failing.run_to_routing()?;
@ -17419,7 +17492,7 @@ mod tests {
let before = now_millis()?;
assert!(failing.step().is_err(), "an unanswered stable route was admitted");
assert_eq!(failing.reloads(), 2, "install then rollback");
assert!(newest(&failing.world)? >= before, "the rollback was not counted");
assert!(newest(&failing.world)? >= before, "the rollback was not recorded");
assert!(!failing.fragment_path().exists(), "the rollback left the candidate's route");
Ok(())
}
@ -17427,32 +17500,37 @@ mod tests {
#[cfg(unix)]
#[test]
fn a_continuity_restart_is_never_refused_by_a_full_ledger() -> Result<()> {
// Warming: the preflight of a continuity runs and is counted.
// Warming: the preflight of a continuity runs and is recorded,
// and the Forward ledger is left as it was.
let warming = routed_world(Odin::Unreachable, 1, DeploymentPhase::Warming, false)?;
warming.step()?;
assert!(warming.transaction()?.warming.is_some());
warming.ledger(ROUTE_ACTUATION_CEILING)?;
let before = *meters_of(&warming.world, "service")?.route_actuations.last().unwrap();
let ledger = meters_of(&warming.world, "service")?.route_actuations;
warming.step()?;
assert!(warming.transaction()?.route_preflight.is_some(), "the preflight was refused");
assert!(*meters_of(&warming.world, "service")?.route_actuations.last().unwrap() > before);
let after = meters_of(&warming.world, "service")?;
assert_eq!(after.route_survivals.len(), 1, "the preflight was not recorded");
assert_eq!(after.route_actuations, ledger, "a survival was counted");
// Routing: its install runs and is counted.
// Routing: its install runs and is recorded, and not counted.
let routing = routed_world(Odin::Unreachable, 1, DeploymentPhase::Fencing, false)?;
routing.stub.set_state("active");
routing.run_to_routing()?;
routing.ledger(ROUTE_ACTUATION_CEILING)?;
let before = *meters_of(&routing.world, "service")?.route_actuations.last().unwrap();
let ledger = meters_of(&routing.world, "service")?.route_actuations;
routing.step()?;
assert!(routing.reloads() >= 1, "the install did not run");
assert!(routing.fragment_path().exists());
assert!(*meters_of(&routing.world, "service")?.route_actuations.last().unwrap() > before);
let after = meters_of(&routing.world, "service")?;
assert_eq!(after.route_survivals.len(), 1, "the install was not recorded");
assert_eq!(after.route_actuations, ledger, "a survival was counted");
Ok(())
}
#[cfg(unix)]
#[test]
fn a_survival_that_cannot_be_counted_runs_and_a_forward_is_refused() -> Result<()> {
fn a_survival_that_cannot_be_recorded_runs_and_a_forward_is_refused() -> Result<()> {
let routed = routed_world(Odin::Unreachable, 1, DeploymentPhase::Fencing, false)?;
let transaction = routed.transaction()?;
let preflight = transaction.route_preflight.clone().context("no preflight")?;
@ -17466,7 +17544,7 @@ mod tests {
.route
.context("no route binding")?,
);
// The ledger cannot be read, so nothing can be counted.
// The ledger cannot be read, so nothing can be recorded.
std::fs::write(&routed.world.state_store, b"unreadable")?;
let engine = &routed.world.engine;
@ -17482,7 +17560,7 @@ mod tests {
&preflight,
&engine.route_gate("service", CommandKind::Deploy),
)?;
assert_eq!(routed.reloads(), 1, "the uncounted withdrawal did not run");
assert_eq!(routed.reloads(), 1, "the unrecorded withdrawal did not run");
Ok(())
}
@ -17669,7 +17747,9 @@ mod tests {
assert_eq!(state.consecutive_failures, 1);
assert!(state.degraded_since_unix_millis.is_some());
assert!(ControlSnapshot::read(&routed.world.state_store)?.transactions.is_empty());
assert_eq!(meters_of(&routed.world, "service")?.route_actuations.len(), 2);
let meters = meters_of(&routed.world, "service")?;
assert_eq!(meters.route_survivals.len(), 2);
assert!(meters.route_actuations.is_empty());
// Tick 2: the route waits, and the dead unit is restarted from a
// fresh read, counted in the same write that mints it.
@ -17724,21 +17804,59 @@ mod tests {
#[cfg(unix)]
#[test]
fn a_candidate_whose_route_proof_fails_reloads_at_most_the_ceiling() -> Result<()> {
fn a_candidate_whose_route_proof_fails_reloads_at_most_twice_the_ceiling() -> Result<()> {
let routed = routed_deploy_world(Odin::Unreachable, 1, DeploymentPhase::Fencing)?;
routed.stub.set_state("active");
routed.run_to_routing()?;
routed.stub.hang_up.store(true, Ordering::SeqCst);
let mut last = None;
for _ in 0..30 {
for _ in 0..40 {
last = Some(routed.step().unwrap_err());
}
// Every install is followed by its rollback and both are counted, so
// twelve reloads spend the window; the rest are refused.
assert_eq!(routed.reloads(), ROUTE_ACTUATION_CEILING);
// Every install is a counted Forward and is followed by one
// recorded rollback, so the rollbacks are bounded by the installs:
// twelve installs spend the window, each reloads twice, and every
// later attempt is refused before it runs anything.
assert_eq!(routed.reloads(), 2 * ROUTE_ACTUATION_CEILING);
let meters = meters_of(&routed.world, "service")?;
assert_eq!(meters.route_used(now_millis()?), ROUTE_ACTUATION_CEILING);
assert_eq!(meters.route_survivals.len(), ROUTE_SURVIVAL_RECORD);
let last = last.unwrap();
assert!(last.downcast_ref::<RouteActuationRefused>().is_some(), "{last:#}");
Ok(())
}
/// The rescue (ruling 3d A): a target whose continuity spent every
/// restart, each charging its preflight and install as Survival, is
/// still deployed: its route changes are the ceiling's first Forward.
#[cfg(unix)]
#[test]
fn a_target_that_spent_its_restarts_can_still_be_deployed() -> Result<()> {
let routed = routed_deploy_world(Odin::Unreachable, 1, DeploymentPhase::Warming)?;
routed.stub.set_state("active");
routed.step()?;
assert!(routed.transaction()?.warming.is_some());
set_meters(
&routed.world,
&restarted_ago(&[3_000_000, 2_400_000, 1_800_000, 1_200_000, 600_000, 200_000])?,
)?;
for _ in 0..2 * CONTINUITY_RESTART_ATTEMPTS {
routed.world.engine.charge_route_actuation("service", RouteActuation::Survival)?;
}
let spent = meters_of(&routed.world, "service")?;
assert!(spent.restarts_exhausted(now_millis()?));
assert_eq!(spent.route_survivals.len(), 2 * CONTINUITY_RESTART_ATTEMPTS);
// Warming's preflight and Routing's install are both admitted.
routed.step()?;
assert!(routed.transaction()?.route_preflight.is_some(), "the preflight was refused");
routed.run_to_routing()?;
routed.promote()?;
assert!(routed.fragment_path().exists(), "the install was refused");
assert!(routed.reloads() >= 1);
let after = meters_of(&routed.world, "service")?;
assert_eq!(after.route_actuations.len(), 2, "preflight and install are the Forward");
Ok(())
}
}
}

View file

@ -5079,10 +5079,10 @@ impl Default for RouteActuators {
}
}
/// Why a route actuation is being asked for. Only a `Forward` change can be
/// refused: restoring the admitted or incumbent route, and every actuation of a
/// continuity, is survival, not deployment, so a `Survival` is always admitted
/// and counted whenever the ledger can record it.
/// Why a route actuation is being asked for. Only a `Forward` change is
/// counted against the ceiling and can be refused: restoring the admitted or
/// incumbent route, and every actuation of a continuity, is survival, not
/// deployment, so a `Survival` is always admitted and only recorded.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RouteActuation {
Forward,