mirror of
https://github.com/telemt/telemt.git
synced 2026-10-10 11:25:57 +03:00
Direct-since-epoch
This commit is contained in:
+6
-22
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user