use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Notify;
use super::org_scoped_store::{
DirtyCapabilities, PrivateDiscoveryChangeBatch, PrivateDiscoveryDrain, PrivateDiscoveryDrains,
PrivateDiscoveryStream,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RoutingHealth {
Healthy { incarnation: u64 },
Rebuilding { incarnation: u64 },
Fenced,
}
impl RoutingHealth {
pub(crate) fn allows(&self, incarnation: u64) -> bool {
matches!(self, RoutingHealth::Healthy { incarnation: live } if *live == incarnation)
}
}
pub(crate) type SharedRoutingHealth = Arc<arc_swap::ArcSwap<RoutingHealth>>;
pub(crate) fn new_routing_health() -> SharedRoutingHealth {
Arc::new(arc_swap::ArcSwap::from_pointee(RoutingHealth::Fenced))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ActorFault {
pub reason: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ApplyOutcome {
Current { source_generation: u64 },
Progress { source_generation: u64 },
Superseded,
#[allow(dead_code)]
Fault(ActorFault),
}
#[derive(Default)]
pub(crate) struct RegistryWork {
pending: AtomicBool,
notify: Notify,
}
impl RegistryWork {
pub(crate) fn mark(&self) {
self.pending.store(true, Ordering::Release);
self.notify.notify_waiters();
}
fn take(&self) -> bool {
self.pending.swap(false, Ordering::AcqRel)
}
#[cfg(test)]
pub(crate) fn take_for_test(&self) -> bool {
self.take()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ApplyRequest {
pub batch: PrivateDiscoveryChangeBatch,
pub registry_work: bool,
}
pub(crate) trait DirtyApply: Send + Sync + 'static {
fn apply(&self, incarnation: u64, request: ApplyRequest) -> ApplyOutcome;
fn activate_incarnation(&self, _incarnation: u64) {}
fn deactivate_incarnation(&self, _incarnation: u64) {}
fn next_deadline(&self) -> Option<u64> {
None
}
fn retire_expired(&self, _now_secs: u64) -> u64 {
0
}
}
pub(crate) type SharedApply = Arc<dyn DirtyApply>;
#[derive(Debug, PartialEq, Eq)]
enum ActorExit {
Shutdown,
SourceClosedUnexpected,
Fault(ActorFault),
}
struct IncarnationFence {
health: SharedRoutingHealth,
apply: SharedApply,
incarnation: u64,
}
impl Drop for IncarnationFence {
fn drop(&mut self) {
self.health.store(Arc::new(RoutingHealth::Fenced));
self.apply.deactivate_incarnation(self.incarnation);
}
}
const RESTART_BACKOFF_BASE: Duration = Duration::from_millis(100);
const RESTART_BACKOFF_CAP: Duration = Duration::from_secs(30);
const MAX_RESTARTS_IN_WINDOW: usize = 5;
const RESTART_WINDOW: Duration = Duration::from_secs(300);
const SUPERSEDED_BACKOFF_AFTER: u32 = 3;
const SUPERSEDED_BACKOFF_BASE: Duration = Duration::from_millis(2);
const SUPERSEDED_BACKOFF_CAP: Duration = Duration::from_millis(250);
const SUPERSEDED_DEGRADED_AT: u32 = 8;
#[cfg(any(test, feature = "fixtures"))]
const MAX_RECORDED_HEALTH_TRANSITIONS: usize = 256;
#[cfg(any(test, feature = "fixtures"))]
#[derive(Default)]
pub(crate) struct ActorHooks {
pub(crate) health_transitions: parking_lot::Mutex<Vec<RoutingHealth>>,
#[allow(clippy::type_complexity)]
pub(crate) drained:
parking_lot::Mutex<Option<Arc<dyn Fn(u64, &PrivateDiscoveryChangeBatch) + Send + Sync>>>,
pub(crate) passes: std::sync::atomic::AtomicU64,
}
#[cfg(any(test, feature = "fixtures"))]
impl ActorHooks {
fn note_health(&self, state: &RoutingHealth) {
let mut transitions = self.health_transitions.lock();
if transitions.len() >= MAX_RECORDED_HEALTH_TRANSITIONS {
transitions.remove(0);
}
transitions.push(*state);
}
fn fire_drained(&self, incarnation: u64, batch: &PrivateDiscoveryChangeBatch) {
if let Some(hook) = self.drained.lock().clone() {
hook(incarnation, batch);
}
}
fn note_pass(&self) {
self.passes.fetch_add(1, Ordering::AcqRel);
}
}
struct Incarnation {
drain: PrivateDiscoveryDrain,
changed: tokio::sync::watch::Receiver<u64>,
health: SharedRoutingHealth,
id: u64,
apply: SharedApply,
work: Arc<RegistryWork>,
shutdown: Arc<AtomicBool>,
shutdown_notify: Arc<Notify>,
metrics: Arc<RoutingMetrics>,
#[cfg(any(test, feature = "fixtures"))]
hooks: Arc<ActorHooks>,
}
async fn run_incarnation(mut it: Incarnation) -> ActorExit {
it.apply.activate_incarnation(it.id);
let _fence = IncarnationFence {
health: it.health.clone(),
apply: it.apply.clone(),
incarnation: it.id,
};
let mut owed_recapture = false;
let mut superseded_streak: u32 = 0;
loop {
let shutdown_signal = it.shutdown_notify.notified();
tokio::pin!(shutdown_signal);
shutdown_signal.as_mut().enable();
if it.shutdown.load(Ordering::Acquire) {
return ActorExit::Shutdown;
}
let work_signal = it.work.notify.notified();
tokio::pin!(work_signal);
work_signal.as_mut().enable();
let registry_work = it.work.take();
it.changed.borrow_and_update();
let mut batch = it.drain.drain();
#[cfg(any(test, feature = "fixtures"))]
it.hooks.fire_drained(it.id, &batch);
if owed_recapture {
batch.dirty = DirtyCapabilities::RebuildAll;
}
let full = matches!(batch.dirty, DirtyCapabilities::RebuildAll);
let quiet = matches!(batch.dirty, DirtyCapabilities::Clean) && !registry_work;
if !quiet {
if full {
let state = RoutingHealth::Rebuilding { incarnation: it.id };
#[cfg(any(test, feature = "fixtures"))]
it.hooks.note_health(&state);
it.health.store(Arc::new(state));
}
match it.apply.apply(
it.id,
ApplyRequest {
batch,
registry_work,
},
) {
ApplyOutcome::Current { .. } => {
if it.shutdown.load(Ordering::Acquire) {
return ActorExit::Shutdown;
}
if full {
let state = RoutingHealth::Healthy { incarnation: it.id };
#[cfg(any(test, feature = "fixtures"))]
it.hooks.note_health(&state);
it.health.store(Arc::new(state));
owed_recapture = false;
}
superseded_streak = 0;
it.metrics.clear_superseded_streak();
}
ApplyOutcome::Progress { .. } => {
owed_recapture = owed_recapture || full;
superseded_streak = 0;
it.metrics.clear_superseded_streak();
}
ApplyOutcome::Superseded => {
owed_recapture = owed_recapture || full;
superseded_streak = superseded_streak.saturating_add(1);
it.metrics.note_superseded_streak(superseded_streak);
if superseded_streak == SUPERSEDED_DEGRADED_AT {
owed_recapture = true;
let state = RoutingHealth::Rebuilding { incarnation: it.id };
#[cfg(any(test, feature = "fixtures"))]
it.hooks.note_health(&state);
it.health.store(Arc::new(state));
it.metrics.note_degraded();
tracing::warn!(
incarnation = it.id,
streak = superseded_streak,
"org routing: reconciliation is not converging; the source is \
moving faster than a quantum can close. Routing reads are cold \
until it settles."
);
}
}
ApplyOutcome::Fault(fault) => return ActorExit::Fault(fault),
}
}
if superseded_streak > SUPERSEDED_BACKOFF_AFTER {
let steps = superseded_streak - SUPERSEDED_BACKOFF_AFTER - 1;
let delay = SUPERSEDED_BACKOFF_BASE
.saturating_mul(1u32 << steps.min(16))
.min(SUPERSEDED_BACKOFF_CAP);
tokio::select! {
_ = tokio::time::sleep(delay) => {}
_ = &mut shutdown_signal => return ActorExit::Shutdown,
}
}
if !quiet {
tokio::task::yield_now().await;
}
let deadline_wait = it.apply.next_deadline().map(|deadline| {
Duration::from_secs(deadline.saturating_sub(super::org::current_timestamp()))
});
if deadline_wait.is_some_and(|wait| wait.is_zero()) {
let retired = it.apply.retire_expired(super::org::current_timestamp());
if retired > 0 {
it.work.mark();
tracing::debug!(retired, "org routing: artifact deadline reached");
}
continue;
}
#[cfg(any(test, feature = "fixtures"))]
it.hooks.note_pass();
tokio::select! {
_ = async {
match deadline_wait {
Some(wait) => tokio::time::sleep(wait).await,
None => std::future::pending::<()>().await,
}
} => {}
_ = &mut work_signal => {}
changed_result = it.changed.changed() => {
if changed_result.is_err() {
return if it.shutdown.load(Ordering::Acquire) {
ActorExit::Shutdown
} else {
ActorExit::SourceClosedUnexpected
};
}
}
_ = &mut shutdown_signal => return ActorExit::Shutdown,
}
}
}
#[derive(Default)]
pub(crate) struct RoutingMetrics {
incarnations: AtomicU64,
source_closed_unexpected: AtomicU64,
superseded_streak: AtomicU64,
max_superseded_streak: AtomicU64,
degraded_entries: AtomicU64,
}
impl RoutingMetrics {
pub(crate) fn incarnations_started(&self) -> u64 {
self.incarnations.load(Ordering::Acquire)
}
pub(crate) fn source_closed_unexpected(&self) -> u64 {
self.source_closed_unexpected.load(Ordering::Acquire)
}
pub(crate) fn superseded_streaks(&self) -> (u64, u64, u64) {
(
self.superseded_streak.load(Ordering::Acquire),
self.max_superseded_streak.load(Ordering::Acquire),
self.degraded_entries.load(Ordering::Acquire),
)
}
fn note_superseded_streak(&self, streak: u32) {
let streak = u64::from(streak);
self.superseded_streak.store(streak, Ordering::Release);
self.max_superseded_streak
.fetch_max(streak, Ordering::AcqRel);
}
fn clear_superseded_streak(&self) {
self.superseded_streak.store(0, Ordering::Release);
}
fn note_degraded(&self) {
self.degraded_entries.fetch_add(1, Ordering::AcqRel);
}
fn next_incarnation(&self) -> Option<u64> {
let mut current = self.incarnations.load(Ordering::Acquire);
loop {
let next = current.checked_add(1)?;
match self.incarnations.compare_exchange_weak(
current,
next,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => return Some(next),
Err(actual) => current = actual,
}
}
}
#[cfg(test)]
fn set_incarnations_for_test(&self, value: u64) {
self.incarnations.store(value, Ordering::Release);
}
}
pub(crate) struct RoutingSupervisor {
mint: PrivateDiscoveryDrains,
health: SharedRoutingHealth,
metrics: Arc<RoutingMetrics>,
}
impl RoutingSupervisor {
pub(crate) fn new(
mint: PrivateDiscoveryDrains,
health: SharedRoutingHealth,
metrics: Arc<RoutingMetrics>,
) -> Self {
Self {
mint,
health,
metrics,
}
}
pub(crate) async fn run(
self,
changed: tokio::sync::watch::Receiver<u64>,
apply: SharedApply,
work: Arc<RegistryWork>,
shutdown: Arc<AtomicBool>,
shutdown_notify: Arc<Notify>,
#[cfg(any(test, feature = "fixtures"))] hooks: Arc<ActorHooks>,
) {
let mut faults: Vec<tokio::time::Instant> = Vec::new();
loop {
if shutdown.load(Ordering::Acquire) {
self.fence();
return;
}
let Some(drain) = self.mint.mint(PrivateDiscoveryStream::Global) else {
self.fence();
tracing::error!(
"org routing: the global private-discovery drain is unavailable; \
routing stays fenced and no actor is started"
);
return;
};
let Some(id) = self.metrics.next_incarnation() else {
drop(drain);
self.fence();
tracing::error!(
"org routing: incarnation counter exhausted; routing stays fenced \
rather than reusing an identifier"
);
return;
};
if shutdown.load(Ordering::Acquire) {
drop(drain);
self.fence();
return;
}
let exit = run_incarnation(Incarnation {
drain,
changed: changed.clone(),
health: self.health.clone(),
id,
apply: apply.clone(),
work: work.clone(),
shutdown: shutdown.clone(),
shutdown_notify: shutdown_notify.clone(),
metrics: self.metrics.clone(),
#[cfg(any(test, feature = "fixtures"))]
hooks: hooks.clone(),
})
.await;
let fault = match exit {
ActorExit::Shutdown => {
self.fence();
return;
}
ActorExit::SourceClosedUnexpected => {
self.metrics
.source_closed_unexpected
.fetch_add(1, Ordering::AcqRel);
self.fence();
tracing::error!(
incarnation = id,
"org routing: the private-discovery change source closed with no \
shutdown in progress; invalidations have stopped while discovery \
can still change. Routing stays fenced."
);
return;
}
ActorExit::Fault(fault) => fault,
};
let now = tokio::time::Instant::now();
faults.retain(|at| now.duration_since(*at) < RESTART_WINDOW);
faults.push(now);
if faults.len() > MAX_RESTARTS_IN_WINDOW {
self.fence();
tracing::error!(
faults = faults.len(),
reason = %fault.reason,
"org routing: actor crash-loop budget exhausted; routing stays \
fenced until the node is restarted"
);
return;
}
let shift = u32::try_from(faults.len()).unwrap_or(u32::MAX).min(16);
let backoff =
RESTART_BACKOFF_CAP.min(RESTART_BACKOFF_BASE.saturating_mul(1u32 << (shift - 1)));
tracing::warn!(
incarnation = id,
?backoff,
reason = %fault.reason,
"org routing: actor incarnation faulted; restarting after backoff"
);
let shutdown_signal = shutdown_notify.notified();
tokio::pin!(shutdown_signal);
shutdown_signal.as_mut().enable();
if shutdown.load(Ordering::Acquire) {
self.fence();
return;
}
tokio::select! {
_ = tokio::time::sleep(backoff) => {}
_ = &mut shutdown_signal => {
self.fence();
return;
}
}
}
}
fn fence(&self) {
self.health.store(Arc::new(RoutingHealth::Fenced));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::adapter::net::behavior::capability::CapabilitySet;
use crate::adapter::net::behavior::org::OrgId;
use crate::adapter::net::behavior::org_scoped_ingest::{
CapabilityAudienceScope, PreparedScopedCapability, VerifiedScopedCapability,
};
use crate::adapter::net::behavior::org_scoped_store::{NoConsumerGrants, ScopedDiscoveryState};
use crate::adapter::net::identity::EntityId;
type Applied = Arc<parking_lot::Mutex<Vec<(u64, DirtyCapabilities, bool)>>>;
type Decide = Box<dyn Fn(u64, &ApplyRequest) -> ApplyOutcome + Send + Sync>;
struct ScriptedApply {
seen: Applied,
decide: Decide,
}
impl DirtyApply for ScriptedApply {
fn apply(&self, incarnation: u64, request: ApplyRequest) -> ApplyOutcome {
self.seen.lock().push((
incarnation,
request.batch.dirty.clone(),
request.registry_work,
));
(self.decide)(incarnation, &request)
}
}
fn owner_record(seed: u8) -> PreparedScopedCapability {
let descriptor = CapabilitySet::new().add_tag("nrpc:x").to_bytes_compact();
PreparedScopedCapability::prepare(VerifiedScopedCapability::for_test(
CapabilityAudienceScope::Owner {
org_id: OrgId::from_bytes([1u8; 32]),
audience_handle: [0x11u8; 32],
},
EntityId::from_bytes([seed; 32]),
OrgId::from_bytes([1u8; 32]),
1,
10_000,
5,
None,
descriptor,
))
}
struct Harness {
state: Arc<parking_lot::Mutex<ScopedDiscoveryState>>,
health: SharedRoutingHealth,
metrics: Arc<RoutingMetrics>,
seen: Applied,
work: Arc<RegistryWork>,
shutdown: Arc<AtomicBool>,
notify: Arc<Notify>,
hooks: Arc<ActorHooks>,
tx: tokio::sync::watch::Sender<u64>,
rx: tokio::sync::watch::Receiver<u64>,
}
fn harness() -> Harness {
let state = Arc::new(parking_lot::Mutex::new(ScopedDiscoveryState::new()));
state.lock().ingest(owner_record(3), 0, &NoConsumerGrants);
let (tx, rx) = tokio::sync::watch::channel(0u64);
Harness {
state,
health: new_routing_health(),
metrics: Arc::default(),
seen: Arc::default(),
work: Arc::default(),
shutdown: Arc::new(AtomicBool::new(false)),
notify: Arc::new(Notify::new()),
hooks: Arc::default(),
tx,
rx,
}
}
impl Harness {
fn supervisor(&self) -> RoutingSupervisor {
RoutingSupervisor::new(
PrivateDiscoveryDrains::new(self.state.clone()),
self.health.clone(),
self.metrics.clone(),
)
}
fn applier(&self, decide: Decide) -> SharedApply {
Arc::new(ScriptedApply {
seen: self.seen.clone(),
decide,
})
}
fn ok_applier(&self) -> SharedApply {
self.applier(Box::new(|_, r| ApplyOutcome::Current {
source_generation: r.batch.generation,
}))
}
fn spawn(&self, sup: RoutingSupervisor, apply: SharedApply) -> tokio::task::JoinHandle<()> {
let (rx, work, shutdown, notify, hooks) = (
self.rx.clone(),
self.work.clone(),
self.shutdown.clone(),
self.notify.clone(),
self.hooks.clone(),
);
tokio::spawn(async move { sup.run(rx, apply, work, shutdown, notify, hooks).await })
}
fn stop(&self) {
self.shutdown.store(true, Ordering::Release);
self.notify.notify_waiters();
}
fn health(&self) -> RoutingHealth {
**self.health.load()
}
fn lease_free(&self) -> bool {
PrivateDiscoveryDrains::new(self.state.clone())
.mint(PrivateDiscoveryStream::Global)
.is_some()
}
}
async fn settle() {
for _ in 0..32 {
tokio::task::yield_now().await;
}
}
#[tokio::test(start_paused = true)]
async fn a_held_stream_fences_instead_of_starting_a_drainless_actor() {
let h = harness();
h.health
.store(Arc::new(RoutingHealth::Healthy { incarnation: 99 }));
let squatter = PrivateDiscoveryDrains::new(h.state.clone());
let _held = squatter
.mint(PrivateDiscoveryStream::Global)
.expect("squatter holds it");
h.supervisor()
.run(
h.rx.clone(),
h.ok_applier(),
h.work.clone(),
h.shutdown.clone(),
h.notify.clone(),
h.hooks.clone(),
)
.await;
assert_eq!(h.metrics.incarnations_started(), 0, "no actor was started");
assert_eq!(h.health(), RoutingHealth::Fenced);
}
#[test]
fn the_health_transition_log_is_bounded_and_keeps_the_newest() {
let hooks = ActorHooks::default();
for incarnation in 0..(MAX_RECORDED_HEALTH_TRANSITIONS as u64 * 3) {
hooks.note_health(&RoutingHealth::Healthy { incarnation });
}
let log = hooks.health_transitions.lock().clone();
assert_eq!(
log.len(),
MAX_RECORDED_HEALTH_TRANSITIONS,
"a fixtures-build node cannot grow this without bound"
);
assert_eq!(
log.last(),
Some(&RoutingHealth::Healthy {
incarnation: MAX_RECORDED_HEALTH_TRANSITIONS as u64 * 3 - 1
}),
"and the NEWEST transition survives — the witnesses read the tail"
);
}
#[tokio::test]
async fn a_sustained_superseded_streak_backs_off_and_reports_degraded() {
let h = harness();
let settled = Arc::new(AtomicBool::new(false));
let applier = {
let work = h.work.clone();
let settled = settled.clone();
h.applier(Box::new(move |_, r| {
if settled.load(Ordering::Acquire) {
return ApplyOutcome::Current {
source_generation: r.batch.generation,
};
}
work.mark();
ApplyOutcome::Superseded
}))
};
let run = h.spawn(h.supervisor(), applier);
let start = tokio::time::Instant::now();
for _ in 0..2_000 {
if h.seen.lock().len() >= 10 {
break;
}
tokio::time::sleep(Duration::from_millis(1)).await;
}
let elapsed = start.elapsed();
assert!(
h.seen.lock().len() >= 10,
"the actor must keep retrying — backing off is not giving up"
);
assert!(
elapsed >= Duration::from_millis(60),
"ten consecutive supersessions must have cost real backoff, not a \
yield-paced spin (elapsed {elapsed:?})"
);
let (current, high_water, degraded) = h.metrics.superseded_streaks();
assert!(
current >= u64::from(SUPERSEDED_DEGRADED_AT),
"the current streak is the live non-convergence signal (was {current})"
);
assert!(high_water >= current, "and the high-water tracks it");
assert_eq!(degraded, 1, "the plane entered DEGRADED exactly once");
assert!(
matches!(h.health(), RoutingHealth::Rebuilding { .. }),
"degraded is COLD: an unbounded rebuild loop must not look healthy \
from outside (health {:?})",
h.health()
);
settled.store(true, Ordering::Release);
h.work.mark();
for _ in 0..2_000 {
if matches!(h.health(), RoutingHealth::Healthy { .. }) {
break;
}
tokio::time::sleep(Duration::from_millis(1)).await;
}
assert!(
matches!(h.health(), RoutingHealth::Healthy { .. }),
"a settled source recovers without a special case (health {:?})",
h.health()
);
assert_eq!(
h.metrics.superseded_streaks().0,
0,
"and the live streak clears, while the high-water survives for diagnosis"
);
assert!(h.metrics.superseded_streaks().1 >= u64::from(SUPERSEDED_DEGRADED_AT));
h.stop();
let _ = run.await;
}
#[tokio::test(start_paused = true)]
async fn shutdown_inside_apply_is_never_followed_by_healthy() {
let h = harness();
let shutdown = h.shutdown.clone();
let notify = h.notify.clone();
let applier = h.applier(Box::new(move |_, r| {
shutdown.store(true, Ordering::Release);
notify.notify_waiters();
ApplyOutcome::Current {
source_generation: r.batch.generation,
}
}));
let run = h.spawn(h.supervisor(), applier);
settle().await;
run.await.expect("supervisor joins");
let transitions = h.hooks.health_transitions.lock().clone();
assert!(
transitions.contains(&RoutingHealth::Rebuilding { incarnation: 1 }),
"the recapture still announced itself: {transitions:?}"
);
assert!(
!transitions
.iter()
.any(|state| matches!(state, RoutingHealth::Healthy { .. })),
"Healthy must NEVER be published after shutdown was observed: {transitions:?}"
);
assert_eq!(h.health(), RoutingHealth::Fenced, "and the exit fences");
}
#[tokio::test(start_paused = true)]
async fn a_recapture_reports_healthy_only_after_current_installation() {
let h = harness();
let run = h.spawn(h.supervisor(), h.ok_applier());
settle().await;
assert_eq!(h.health(), RoutingHealth::Healthy { incarnation: 1 });
assert_eq!(
h.seen.lock().as_slice(),
&[(1, DirtyCapabilities::RebuildAll, false)],
"the first batch is the mint's complete recapture"
);
h.stop();
run.await.expect("supervisor joins");
assert_eq!(h.health(), RoutingHealth::Fenced, "exit fences");
}
#[tokio::test(start_paused = true)]
async fn a_superseded_recapture_never_publishes_healthy() {
let h = harness();
let apply = h.applier(Box::new(|_, _| ApplyOutcome::Superseded));
let run = h.spawn(h.supervisor(), apply);
settle().await;
assert_eq!(
h.health(),
RoutingHealth::Rebuilding { incarnation: 1 },
"an obsolete attempt leaves the actor in recapture, never Healthy"
);
h.stop();
let _ = tokio::time::timeout(Duration::from_secs(5), run).await;
}
#[tokio::test(start_paused = true)]
async fn an_owed_recapture_survives_the_caps_wake_that_superseded_it() {
let h = harness();
let attempts = Arc::new(AtomicU64::new(0));
let apply = {
let (attempts, state, tx) = (attempts.clone(), h.state.clone(), h.tx.clone());
h.applier(Box::new(move |_, r| {
if attempts.fetch_add(1, Ordering::AcqRel) == 0 {
state.lock().ingest(owner_record(4), 0, &NoConsumerGrants);
let _ = tx.send(1);
ApplyOutcome::Superseded
} else {
ApplyOutcome::Current {
source_generation: r.batch.generation,
}
}
}))
};
let run = h.spawn(h.supervisor(), apply);
settle().await;
assert_eq!(
h.seen.lock().as_slice(),
&[
(1, DirtyCapabilities::RebuildAll, false),
(1, DirtyCapabilities::RebuildAll, false)
],
"the second attempt must receive RebuildAll — the owed recapture \
subsumes the Caps delta that woke the actor"
);
assert_eq!(
h.health(),
RoutingHealth::Healthy { incarnation: 1 },
"the recapture completed and health recovered"
);
assert_eq!(attempts.load(Ordering::Acquire), 2, "exactly two attempts");
h.stop();
run.await.expect("supervisor joins");
}
#[tokio::test(start_paused = true)]
async fn caps_movement_leaves_global_health_alone() {
let h = harness();
let during: Arc<parking_lot::Mutex<Vec<(DirtyCapabilities, RoutingHealth)>>> =
Arc::default();
let apply = {
let (during, health) = (during.clone(), h.health.clone());
h.applier(Box::new(move |_, r| {
during.lock().push((r.batch.dirty.clone(), **health.load()));
ApplyOutcome::Current {
source_generation: r.batch.generation,
}
}))
};
let run = h.spawn(h.supervisor(), apply);
settle().await;
assert_eq!(h.health(), RoutingHealth::Healthy { incarnation: 1 });
h.state.lock().ingest(owner_record(4), 0, &NoConsumerGrants);
let _ = h.tx.send(1);
settle().await;
let during = during.lock().clone();
let caps_health = during
.iter()
.find(|(d, _)| matches!(d, DirtyCapabilities::Caps(_)))
.map(|(_, health)| *health)
.expect("a Caps batch was applied");
assert_eq!(
caps_health,
RoutingHealth::Healthy { incarnation: 1 },
"ordinary Caps movement must not globally fence warmed routes while it \
rebuilds — per-slot invalidation is the registry's job"
);
let full_health = during
.iter()
.find(|(d, _)| matches!(d, DirtyCapabilities::RebuildAll))
.map(|(_, health)| *health)
.expect("a RebuildAll batch was applied");
assert_eq!(
full_health,
RoutingHealth::Rebuilding { incarnation: 1 },
"a complete recapture DOES publish global Rebuilding"
);
h.stop();
run.await.expect("supervisor joins");
}
#[tokio::test(start_paused = true)]
async fn a_fault_fences_then_a_successor_recaptures() {
let h = harness();
let apply = h.applier(Box::new(|inc, r| {
if inc == 1 {
ApplyOutcome::Fault(ActorFault {
reason: ("injected").into(),
})
} else {
ApplyOutcome::Current {
source_generation: r.batch.generation,
}
}
}));
let run = h.spawn(h.supervisor(), apply);
settle().await;
tokio::time::advance(Duration::from_secs(1)).await;
settle().await;
assert_eq!(h.metrics.incarnations_started(), 2, "exactly one successor");
assert_eq!(h.health(), RoutingHealth::Healthy { incarnation: 2 });
assert_eq!(
h.seen.lock().as_slice(),
&[
(1, DirtyCapabilities::RebuildAll, false),
(2, DirtyCapabilities::RebuildAll, false)
],
"the successor recaptured completely"
);
h.stop();
run.await.expect("supervisor joins");
}
#[tokio::test(start_paused = true)]
async fn an_abnormal_exit_fences_synchronously_during_backoff() {
let h = harness();
let apply = h.applier(Box::new(|_, _| {
ApplyOutcome::Fault(ActorFault {
reason: ("injected").into(),
})
}));
let run = h.spawn(h.supervisor(), apply);
settle().await;
assert_eq!(
h.health(),
RoutingHealth::Fenced,
"a dead incarnation fences immediately, before any successor"
);
assert_eq!(h.metrics.incarnations_started(), 1, "still in backoff");
h.stop();
let _ = tokio::time::timeout(Duration::from_secs(5), run).await;
}
#[tokio::test(start_paused = true)]
async fn cancelling_the_supervisor_drops_the_incarnation_and_frees_the_lease() {
let h = harness();
let run = h.spawn(h.supervisor(), h.ok_applier());
settle().await;
assert_eq!(h.health(), RoutingHealth::Healthy { incarnation: 1 });
assert!(!h.lease_free(), "the live incarnation holds the lease");
run.abort();
let _ = run.await;
settle().await;
assert_eq!(
h.health(),
RoutingHealth::Fenced,
"cancelling the supervisor fences: no orphan keeps routes usable"
);
assert!(
h.lease_free(),
"the orphaned incarnation did not survive holding the exclusive drain"
);
}
#[tokio::test(start_paused = true)]
async fn a_closed_watch_without_shutdown_is_loud_and_terminal() {
let mut h = harness();
let run = h.spawn(h.supervisor(), h.ok_applier());
settle().await;
let (tx, rx) = tokio::sync::watch::channel(0u64);
drop(std::mem::replace(&mut h.tx, tx));
drop(std::mem::replace(&mut h.rx, rx));
let joined = tokio::time::timeout(Duration::from_secs(5), run).await;
assert!(joined.is_ok(), "terminal, and no busy loop");
assert!(
!h.shutdown.load(Ordering::Acquire),
"this was NOT a shutdown"
);
assert_eq!(
h.metrics.source_closed_unexpected(),
1,
"the abnormal closure is observable"
);
assert_eq!(
h.metrics.incarnations_started(),
1,
"no restart against a permanently closed receiver"
);
assert_eq!(h.health(), RoutingHealth::Fenced);
}
#[tokio::test(start_paused = true)]
async fn a_closed_watch_during_shutdown_is_normal_teardown() {
let mut h = harness();
let run = h.spawn(h.supervisor(), h.ok_applier());
settle().await;
h.shutdown.store(true, Ordering::Release);
let (tx, rx) = tokio::sync::watch::channel(0u64);
drop(std::mem::replace(&mut h.tx, tx));
drop(std::mem::replace(&mut h.rx, rx));
let joined = tokio::time::timeout(Duration::from_secs(5), run).await;
assert!(joined.is_ok());
assert_eq!(
h.metrics.source_closed_unexpected(),
0,
"teardown is not an abnormal closure"
);
assert_eq!(h.health(), RoutingHealth::Fenced);
}
#[tokio::test(start_paused = true)]
async fn shutdown_while_parked_stops_and_fences() {
let h = harness();
let run = h.spawn(h.supervisor(), h.ok_applier());
settle().await;
h.stop();
let joined = tokio::time::timeout(Duration::from_secs(5), run).await;
assert!(joined.is_ok(), "a parked actor still observes shutdown");
assert_eq!(h.metrics.incarnations_started(), 1);
assert_eq!(h.health(), RoutingHealth::Fenced);
}
#[tokio::test(start_paused = true)]
async fn shutdown_during_backoff_starts_no_replacement() {
let h = harness();
let apply = h.applier(Box::new(|inc, r| {
if inc == 1 {
ApplyOutcome::Fault(ActorFault {
reason: ("injected").into(),
})
} else {
ApplyOutcome::Current {
source_generation: r.batch.generation,
}
}
}));
let run = h.spawn(h.supervisor(), apply);
settle().await;
h.stop();
let joined = tokio::time::timeout(Duration::from_secs(5), run).await;
assert!(joined.is_ok(), "backoff is interruptible by shutdown");
assert_eq!(
h.metrics.incarnations_started(),
1,
"no replacement after shutdown"
);
assert_eq!(h.health(), RoutingHealth::Fenced);
}
#[tokio::test(start_paused = true)]
async fn a_deterministic_fault_exhausts_the_restart_budget_and_stays_fenced() {
let h = harness();
let apply = h.applier(Box::new(|_, _| {
ApplyOutcome::Fault(ActorFault {
reason: ("deterministic").into(),
})
}));
let run = h.spawn(h.supervisor(), apply);
let joined = tokio::time::timeout(Duration::from_secs(600), run).await;
assert!(
joined.is_ok(),
"the supervisor gives up rather than spinning"
);
assert_eq!(
h.metrics.incarnations_started() as usize,
MAX_RESTARTS_IN_WINDOW + 1,
"exactly the budgeted attempts, then stop"
);
assert_eq!(
h.health(),
RoutingHealth::Fenced,
"crash-loop exhaustion is fail-closed"
);
}
#[tokio::test(start_paused = true)]
async fn incarnation_overflow_fences_rather_than_reusing_an_id() {
let h = harness();
h.metrics.set_incarnations_for_test(u64::MAX);
h.health
.store(Arc::new(RoutingHealth::Healthy { incarnation: 7 }));
h.supervisor()
.run(
h.rx.clone(),
h.ok_applier(),
h.work.clone(),
h.shutdown.clone(),
h.notify.clone(),
h.hooks.clone(),
)
.await;
assert_eq!(h.health(), RoutingHealth::Fenced);
assert!(h.lease_free(), "the refused mint released its claim");
}
#[tokio::test(start_paused = true)]
async fn registry_work_reconciles_even_with_a_clean_source_batch() {
let h = harness();
let run = h.spawn(h.supervisor(), h.ok_applier());
settle().await;
let after_recapture = h.seen.lock().len();
h.work.mark();
settle().await;
let seen = h.seen.lock().clone();
assert_eq!(
seen.len(),
after_recapture + 1,
"the registry-work wake produced exactly one reconciliation pass"
);
let (_, dirty, work) = seen.last().expect("a pass").clone();
assert_eq!(
dirty,
DirtyCapabilities::Clean,
"the source really was clean — this pass is work-driven only"
);
assert!(work, "and the pass carries the registry-work trigger");
h.stop();
run.await.expect("supervisor joins");
}
#[tokio::test(start_paused = true)]
async fn registry_work_coalesces_and_is_never_lost() {
let h = harness();
let run = h.spawn(h.supervisor(), h.ok_applier());
settle().await;
let baseline = h.seen.lock().len();
for _ in 0..8 {
h.work.mark();
}
settle().await;
let seen = h.seen.lock().clone();
assert_eq!(
seen.len(),
baseline + 1,
"eight marks coalesce into one pass, not eight: {seen:?}"
);
assert!(
seen.last().expect("a pass").2,
"the coalesced pass carries the work trigger"
);
settle().await;
assert_eq!(
h.seen.lock().len(),
baseline + 1,
"a consumed flag does not re-trigger"
);
h.stop();
run.await.expect("supervisor joins");
}
#[tokio::test(start_paused = true)]
async fn registry_work_marked_before_start_is_not_lost() {
let h = harness();
h.work.mark();
let run = h.spawn(h.supervisor(), h.ok_applier());
settle().await;
let seen = h.seen.lock().clone();
assert_eq!(
seen.first().expect("a first pass"),
&(1, DirtyCapabilities::RebuildAll, true),
"the first pass carries the mint's RebuildAll AND the pre-start work \
flag: {seen:?}"
);
h.stop();
run.await.expect("supervisor joins");
}
#[tokio::test(start_paused = true)]
async fn registry_work_marked_during_an_application_is_consumed_exactly_once() {
let h = harness();
let marked = Arc::new(AtomicBool::new(false));
let apply = {
let (marked, work) = (marked.clone(), h.work.clone());
h.applier(Box::new(move |_, r| {
if !marked.swap(true, Ordering::AcqRel) {
work.mark();
}
ApplyOutcome::Current {
source_generation: r.batch.generation,
}
}))
};
let run = h.spawn(h.supervisor(), apply);
settle().await;
let seen = h.seen.lock().clone();
assert_eq!(
seen.len(),
2,
"exactly one follow-up pass for the queued work: {seen:?}"
);
assert_eq!(
seen[0],
(1, DirtyCapabilities::RebuildAll, false),
"the in-flight pass never saw the mark it had already passed"
);
assert_eq!(
seen[1],
(1, DirtyCapabilities::Clean, true),
"the queued work is consumed by a later pass, with a clean source"
);
h.stop();
run.await.expect("supervisor joins");
}
#[tokio::test(start_paused = true)]
async fn registry_work_neither_fakes_a_rebuild_nor_fences() {
let h = harness();
let run = h.spawn(h.supervisor(), h.ok_applier());
settle().await;
assert_eq!(h.health(), RoutingHealth::Healthy { incarnation: 1 });
h.work.mark();
settle().await;
let (_, dirty, _) = h.seen.lock().last().expect("a pass").clone();
assert_ne!(
dirty,
DirtyCapabilities::RebuildAll,
"first demand must not be synthesized into a node-wide RebuildAll"
);
assert_eq!(
h.health(),
RoutingHealth::Healthy { incarnation: 1 },
"registry work does not globally fence warmed routes"
);
h.stop();
run.await.expect("supervisor joins");
}
#[test]
fn only_the_live_incarnations_routes_are_usable() {
assert!(RoutingHealth::Healthy { incarnation: 7 }.allows(7));
assert!(
!RoutingHealth::Healthy { incarnation: 8 }.allows(7),
"a dead incarnation's routes are never trusted after a successor starts"
);
assert!(!RoutingHealth::Rebuilding { incarnation: 7 }.allows(7));
assert!(!RoutingHealth::Fenced.allows(7));
}
#[cfg(panic = "unwind")]
#[tokio::test(start_paused = true)]
async fn unwind_only_a_panicking_apply_still_runs_the_fence_guard() {
let h = harness();
let apply = h.applier(Box::new(|_, _| panic!("unwind-only fault")));
let health = h.health.clone();
let (rx, work, shutdown, notify, hooks) = (
h.rx.clone(),
h.work.clone(),
h.shutdown.clone(),
h.notify.clone(),
h.hooks.clone(),
);
let sup = h.supervisor();
let run =
tokio::spawn(async move { sup.run(rx, apply, work, shutdown, notify, hooks).await });
let outcome = run.await;
assert!(outcome.is_err(), "the panic propagated (unwind profile)");
assert_eq!(
**health.load(),
RoutingHealth::Fenced,
"the actor-stack fence ran during the unwind"
);
}
}