Skip to main content

phantom_protocol/transport/
fallback.rs

1//! Phantom Protocol - Fallback State Machine
2//!
3//! A degrade/heal state machine over three abstract transport "modes"
4//! (`Turbo → Reliable → Stealth`): on repeated connection failures it walks one
5//! step toward the most-robust mode, and probes back up to the best mode when the
6//! path heals. The mode names predate the PhantomUDP rewrite — the concrete
7//! KCP / TCP / FakeTLS legs they referred to are gone (PhantomUDP is now the only
8//! production transport, with TCP/WebSocket/WASI/Embedded/MimicTls byte-pipes).
9//!
10//! **Vestigial.** The machine is still constructed inside `Session` (held behind
11//! `#[allow(dead_code)]`) but no longer steers a live transport — the project does
12//! single-path connection migration, not transport-mode switching. The logic and
13//! tests are kept intact in case mode-switching is rewired against the current
14//! transports; treat the `Turbo`/`Reliable`/`Stealth` names as opaque tiers, not
15//! as the retired legs.
16
17use crate::transport::scheduler::Scheduler;
18use parking_lot::RwLock;
19use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
20use std::sync::Arc;
21use std::time::Instant;
22
23/// Abstract transport-robustness tiers, most→least performant. The names are
24/// historical (they once mapped to the retired KCP / TCP / FakeTLS legs) and are
25/// now opaque ordering labels for the degrade/heal walk, not concrete transports.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum TransportMode {
28    /// Most performant, least robust tier (the top of the fallback ladder).
29    Turbo,
30    /// Middle tier — trades some performance for robustness.
31    Reliable,
32    /// Most robust, most obfuscated tier (the bottom of the ladder; the machine
33    /// degrades no further once here).
34    Stealth,
35}
36
37/// Conditions that trigger a fallback
38#[derive(Debug, Clone)]
39pub struct FallbackTrigger {
40    pub max_rtt: u32,
41    pub max_loss: u8,
42    pub failure_threshold: u32,
43}
44
45impl Default for FallbackTrigger {
46    fn default() -> Self {
47        Self {
48            max_rtt: 500,
49            max_loss: 10,
50            failure_threshold: 3,
51        }
52    }
53}
54
55#[derive(Debug, Default)]
56pub struct FallbackMetrics {
57    pub packets_sent: AtomicU64,
58    pub packets_acked: AtomicU64,
59    pub connection_failures: AtomicU32,
60    pub last_success_ms: AtomicU64,
61}
62
63impl FallbackMetrics {
64    pub fn record_sent(&self) {
65        self.packets_sent.fetch_add(1, Ordering::Relaxed);
66    }
67
68    pub fn record_success(&self) {
69        self.packets_acked.fetch_add(1, Ordering::Relaxed);
70        self.connection_failures.store(0, Ordering::Relaxed);
71        let now = std::time::SystemTime::now()
72            .duration_since(std::time::UNIX_EPOCH)
73            .map(|d| d.as_millis() as u64)
74            .unwrap_or_default();
75        self.last_success_ms.store(now, Ordering::Relaxed);
76    }
77
78    pub fn record_failure(&self) {
79        self.connection_failures.fetch_add(1, Ordering::Relaxed);
80    }
81}
82
83/// Fallback state machine
84pub struct FallbackStateMachine {
85    /// Current transport mode
86    current_mode: RwLock<TransportMode>,
87    /// Trigger conditions
88    trigger: FallbackTrigger,
89    /// Metrics for decisions
90    metrics: FallbackMetrics,
91    /// Last mode change time
92    last_change: RwLock<Instant>,
93    /// Last probe attempt time
94    last_probe: RwLock<Instant>,
95    /// Best available mode to aim for
96    best_mode: TransportMode,
97    /// Optional scheduler the machine would push mode changes into. Always `None`
98    /// today (the vestigial scheduler is not wired to mode switching) — kept as the
99    /// seam for re-wiring mode-switching against a live transport.
100    #[allow(dead_code)]
101    scheduler: Option<Arc<Scheduler>>,
102}
103
104impl FallbackStateMachine {
105    pub fn with_defaults() -> Self {
106        Self::new(FallbackTrigger::default())
107    }
108
109    pub fn new(trigger: FallbackTrigger) -> Self {
110        Self {
111            current_mode: RwLock::new(TransportMode::Turbo),
112            best_mode: TransportMode::Turbo,
113            trigger,
114            metrics: FallbackMetrics::default(),
115            last_change: RwLock::new(Instant::now()),
116            last_probe: RwLock::new(Instant::now()),
117            scheduler: None,
118        }
119    }
120
121    pub fn metrics(&self) -> &FallbackMetrics {
122        &self.metrics
123    }
124
125    pub fn current_mode(&self) -> TransportMode {
126        *self.current_mode.read()
127    }
128
129    pub fn check_and_fallback(&self) -> bool {
130        let failures = self.metrics.connection_failures.load(Ordering::Relaxed);
131        if failures >= self.trigger.failure_threshold {
132            self.degrade();
133            return true;
134        }
135        false
136    }
137
138    pub fn record_failure(&self) {
139        self.metrics.record_failure();
140        let _ = self.check_and_fallback();
141    }
142
143    fn degrade(&self) {
144        let mut mode = self.current_mode.write();
145        let new_mode = match *mode {
146            TransportMode::Turbo => TransportMode::Reliable,
147            TransportMode::Reliable => TransportMode::Stealth,
148            TransportMode::Stealth => TransportMode::Stealth,
149        };
150
151        if new_mode != *mode {
152            log::warn!("Transport degradation: {:?} -> {:?}", *mode, new_mode);
153            *mode = new_mode;
154            *self.last_change.write() = Instant::now();
155        }
156    }
157
158    pub fn upgrade(&self) {
159        let mut mode = self.current_mode.write();
160        if *mode != self.best_mode {
161            log::info!("Transport healing: {:?} -> {:?}", *mode, self.best_mode);
162            *mode = self.best_mode;
163            *self.last_change.write() = Instant::now();
164            // Reset failures on upgrade
165            self.metrics.connection_failures.store(0, Ordering::Relaxed);
166        }
167    }
168
169    pub fn should_probe(&self) -> bool {
170        let mode = self.current_mode.read();
171        if *mode == self.best_mode {
172            return false;
173        }
174
175        let last_probe = self.last_probe.read();
176        let last_change = self.last_change.read();
177
178        // Probe if it's been > 30s since last probe AND since last mode change
179        last_probe.elapsed() > std::time::Duration::from_secs(30)
180            && last_change.elapsed() > std::time::Duration::from_secs(30)
181    }
182
183    pub fn record_probe(&self) {
184        *self.last_probe.write() = Instant::now();
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_fallback_cycle() {
194        let fsm = FallbackStateMachine::with_defaults();
195        assert_eq!(fsm.current_mode(), TransportMode::Turbo);
196
197        // Degrade to Reliable
198        fsm.degrade();
199        assert_eq!(fsm.current_mode(), TransportMode::Reliable);
200
201        // Degrade to Stealth
202        fsm.degrade();
203        assert_eq!(fsm.current_mode(), TransportMode::Stealth);
204
205        // Upgrade back to Turbo
206        fsm.upgrade();
207        assert_eq!(fsm.current_mode(), TransportMode::Turbo);
208    }
209
210    #[test]
211    fn test_should_probe() {
212        let fsm = FallbackStateMachine::with_defaults();
213
214        // No probe needed if already in best mode
215        assert!(!fsm.should_probe());
216
217        fsm.degrade();
218        assert_eq!(fsm.current_mode(), TransportMode::Reliable);
219
220        // No probe immediately after change
221        assert!(!fsm.should_probe());
222
223        // We can't easily fast-forward time in these tests without mocking Instant
224        // But we can verify that after reset it's still false
225        fsm.record_probe();
226        assert!(!fsm.should_probe());
227    }
228}