Direct-since-epoch

This commit is contained in:
Alexey
2026-09-15 20:37:41 +03:00
parent 5ec9f85530
commit 55f3d19ee0
9 changed files with 483 additions and 73 deletions
+6 -22
View File
@@ -1,5 +1,4 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::watch;
@@ -24,11 +23,11 @@ impl RelayRouteMode {
pub(crate) struct RouteCutoverState {
pub mode: RelayRouteMode,
pub generation: u64,
pub direct_since_epoch_secs: Option<u64>,
}
#[derive(Clone)]
pub(crate) struct RouteRuntimeController {
direct_since_epoch_secs: Arc<AtomicU64>,
tx: watch::Sender<RouteCutoverState>,
}
@@ -37,17 +36,11 @@ impl RouteRuntimeController {
let initial = RouteCutoverState {
mode: initial_mode,
generation: 0,
direct_since_epoch_secs: matches!(initial_mode, RelayRouteMode::Direct)
.then(now_epoch_secs),
};
let (tx, _rx) = watch::channel(initial);
let direct_since_epoch_secs = if matches!(initial_mode, RelayRouteMode::Direct) {
now_epoch_secs()
} else {
0
};
Self {
direct_since_epoch_secs: Arc::new(AtomicU64::new(direct_since_epoch_secs)),
tx,
}
Self { tx }
}
pub(crate) fn snapshot(&self) -> RouteCutoverState {
@@ -58,25 +51,16 @@ impl RouteRuntimeController {
self.tx.subscribe()
}
pub(crate) fn direct_since_epoch_secs(&self) -> Option<u64> {
let value = self.direct_since_epoch_secs.load(Ordering::Relaxed);
(value > 0).then_some(value)
}
pub(crate) fn set_mode(&self, mode: RelayRouteMode) -> Option<RouteCutoverState> {
let mut next = None;
let changed = self.tx.send_if_modified(|state| {
if state.mode == mode {
return false;
}
if matches!(mode, RelayRouteMode::Direct) {
self.direct_since_epoch_secs
.store(now_epoch_secs(), Ordering::Relaxed);
} else {
self.direct_since_epoch_secs.store(0, Ordering::Relaxed);
}
state.mode = mode;
state.generation = state.generation.saturating_add(1);
state.direct_since_epoch_secs =
matches!(mode, RelayRouteMode::Direct).then(now_epoch_secs);
next = Some(*state);
true
});
@@ -9,7 +9,7 @@ fn positive_direct_cutover_sets_timestamp_and_snapshot_coherently() {
let rx = runtime.subscribe();
assert!(
runtime.direct_since_epoch_secs().is_none(),
runtime.snapshot().direct_since_epoch_secs.is_none(),
"middle startup must not expose direct-since timestamp"
);
@@ -24,7 +24,7 @@ fn positive_direct_cutover_sets_timestamp_and_snapshot_coherently() {
);
assert_eq!(observed.mode, RelayRouteMode::Direct);
assert!(
runtime.direct_since_epoch_secs().is_some(),
observed.direct_since_epoch_secs.is_some(),
"direct cutover must publish a non-empty direct-since timestamp"
);
}
@@ -34,12 +34,12 @@ fn negative_idempotent_set_mode_does_not_mutate_timestamp_or_generation() {
let runtime = RouteRuntimeController::new(RelayRouteMode::Direct);
let before_state = runtime.snapshot();
let before_ts = runtime.direct_since_epoch_secs();
let before_ts = before_state.direct_since_epoch_secs;
let changed = runtime.set_mode(RelayRouteMode::Direct);
let after_state = runtime.snapshot();
let after_ts = runtime.direct_since_epoch_secs();
let after_ts = after_state.direct_since_epoch_secs;
assert!(changed.is_none(), "idempotent set_mode must return None");
assert_eq!(
@@ -58,7 +58,7 @@ fn edge_middle_cutover_clears_timestamp() {
let rx = runtime.subscribe();
assert!(
runtime.direct_since_epoch_secs().is_some(),
runtime.snapshot().direct_since_epoch_secs.is_some(),
"direct startup must expose direct-since timestamp"
);
@@ -73,7 +73,7 @@ fn edge_middle_cutover_clears_timestamp() {
);
assert_eq!(observed.mode, RelayRouteMode::Middle);
assert!(
runtime.direct_since_epoch_secs().is_none(),
observed.direct_since_epoch_secs.is_none(),
"middle cutover must clear direct-since timestamp"
);
}
@@ -90,7 +90,7 @@ fn adversarial_blackhat_probe_sequence_observes_consistent_mode_timestamp_pairs(
let observed_direct = *rx.borrow();
assert_eq!(observed_direct, emitted_direct);
assert!(
runtime.direct_since_epoch_secs().is_some(),
observed_direct.direct_since_epoch_secs.is_some(),
"direct observation must never expose empty timestamp"
);
@@ -100,7 +100,7 @@ fn adversarial_blackhat_probe_sequence_observes_consistent_mode_timestamp_pairs(
let observed_middle = *rx.borrow();
assert_eq!(observed_middle, emitted_middle);
assert!(
runtime.direct_since_epoch_secs().is_none(),
observed_middle.direct_since_epoch_secs.is_none(),
"middle observation must never expose direct timestamp"
);
}
@@ -136,9 +136,9 @@ fn integration_subscriber_and_runtime_gates_stay_coherent_across_cutovers() {
assert_eq!(snapshot, emitted);
if matches!(mode, RelayRouteMode::Direct) {
assert!(runtime.direct_since_epoch_secs().is_some());
assert!(snapshot.direct_since_epoch_secs.is_some());
} else {
assert!(runtime.direct_since_epoch_secs().is_none());
assert!(snapshot.direct_since_epoch_secs.is_none());
}
}
}
@@ -176,12 +176,12 @@ fn light_fuzz_random_mode_plan_preserves_timestamp_and_generation_invariants() {
if matches!(snapshot.mode, RelayRouteMode::Direct) {
assert!(
runtime.direct_since_epoch_secs().is_some(),
snapshot.direct_since_epoch_secs.is_some(),
"direct fuzz state must expose timestamp"
);
} else {
assert!(
runtime.direct_since_epoch_secs().is_none(),
snapshot.direct_since_epoch_secs.is_none(),
"middle fuzz state must clear timestamp"
);
}
@@ -199,12 +199,17 @@ fn stress_parallel_subscribers_never_observe_generation_regression() {
let rx = runtime.subscribe();
let mut last = rx.borrow().generation;
for _ in 0..10_000usize {
let current = rx.borrow().generation;
let current = *rx.borrow();
assert!(
current >= last,
current.generation >= last,
"watch generation must be monotonic for every subscriber"
);
last = current;
assert_eq!(
matches!(current.mode, RelayRouteMode::Direct),
current.direct_since_epoch_secs.is_some(),
"one route snapshot must contain a coherent mode/timestamp pair"
);
last = current.generation;
std::thread::yield_now();
}
}));
@@ -227,8 +232,8 @@ fn stress_parallel_subscribers_never_observe_generation_regression() {
let final_state = runtime.snapshot();
if matches!(final_state.mode, RelayRouteMode::Direct) {
assert!(runtime.direct_since_epoch_secs().is_some());
assert!(final_state.direct_since_epoch_secs.is_some());
} else {
assert!(runtime.direct_since_epoch_secs().is_none());
assert!(final_state.direct_since_epoch_secs.is_none());
}
}
+9 -5
View File
@@ -120,6 +120,7 @@ fn session_is_not_affected_when_mode_matches_even_if_generation_advanced() {
let current = RouteCutoverState {
mode: RelayRouteMode::Direct,
generation: 2,
direct_since_epoch_secs: Some(1),
};
let session_generation = 0;
@@ -134,6 +135,7 @@ fn cutover_predicate_rejects_equal_generation_even_if_mode_differs() {
let current = RouteCutoverState {
mode: RelayRouteMode::Middle,
generation: 77,
direct_since_epoch_secs: None,
};
assert!(
!is_session_affected_by_cutover(current, RelayRouteMode::Direct, 77),
@@ -168,13 +170,15 @@ fn adversarial_route_oscillation_only_cuts_over_sessions_with_different_final_mo
fn light_fuzz_cutover_predicate_matches_reference_oracle() {
let mut rng = StdRng::seed_from_u64(0xC0DEC0DE5EED);
for _ in 0..20_000 {
let mode = if rng.random::<bool>() {
RelayRouteMode::Direct
} else {
RelayRouteMode::Middle
};
let current = RouteCutoverState {
mode: if rng.random::<bool>() {
RelayRouteMode::Direct
} else {
RelayRouteMode::Middle
},
mode,
generation: rng.random_range(0u64..1_000_000),
direct_since_epoch_secs: matches!(mode, RelayRouteMode::Direct).then_some(1),
};
let session_mode = if rng.random::<bool>() {
RelayRouteMode::Direct