Skip to main content

nntp_proxy/session/
mode_state.rs

1//! Session mode state management
2//!
3//! This module provides a type-safe wrapper for session mode and routing mode,
4//! ensuring valid state transitions and clear mode switching logic.
5
6use crate::config::RoutingMode;
7use std::sync::atomic::{AtomicU8, Ordering};
8
9/// Session mode - determines how commands are routed
10///
11/// This is separate from `RoutingMode` (configuration) - it represents the
12/// *current* runtime state of the session, which can change (e.g., hybrid mode).
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[repr(u8)]
15pub enum SessionMode {
16    /// Per-command routing - each command may go to different backend
17    PerCommand = 0,
18
19    /// Stateful mode - using a dedicated backend connection
20    Stateful = 1,
21}
22
23impl SessionMode {
24    /// Check if this mode is per-command routing
25    #[inline]
26    #[must_use]
27    pub const fn is_per_command(self) -> bool {
28        matches!(self, Self::PerCommand)
29    }
30
31    /// Check if this mode is stateful
32    #[inline]
33    #[must_use]
34    pub const fn is_stateful(self) -> bool {
35        matches!(self, Self::Stateful)
36    }
37
38    /// Convert to u8 for atomic storage
39    #[inline]
40    const fn to_u8(self) -> u8 {
41        self as u8
42    }
43
44    /// Convert from u8 from atomic storage
45    #[inline]
46    const fn from_u8(value: u8) -> Self {
47        match value {
48            1 => Self::Stateful,
49            _ => Self::PerCommand, // 0 or any unknown value
50        }
51    }
52}
53
54/// Manages session mode state with support for runtime transitions
55///
56/// This type encapsulates the current session mode and routing mode configuration,
57/// providing thread-safe mode transitions for hybrid routing.
58///
59/// # Design
60///
61/// - **Current Mode**: `AtomicU8` for lock-free concurrent reads/writes
62/// - **Routing Mode**: Immutable configuration (Stateful, `PerCommand`, or Hybrid)
63/// - Mode transitions are only allowed in Hybrid mode
64///
65/// # One-Way Transition Invariant
66///
67/// **CRITICAL**: In Hybrid mode, the transition from `PerCommand` → Stateful is
68/// **permanent and irreversible** for the lifetime of the connection:
69///
70/// ```text
71/// PerCommand ──stateful command──> Stateful
72///     ↑                               │
73///     └───────── NO WAY BACK ─────────┘
74/// ```
75///
76/// Once `switch_to_stateful()` is called:
77/// - Connection acquires a dedicated backend
78/// - All subsequent commands use that backend
79/// - Connection stays stateful until client disconnects
80/// - New client connection starts fresh in `PerCommand` mode (if Hybrid)
81///
82/// # Examples
83///
84/// ```
85/// use nntp_proxy::session::{ModeState, SessionMode};
86/// use nntp_proxy::config::RoutingMode;
87///
88/// // Stateful mode (no transitions allowed)
89/// let state = ModeState::new(SessionMode::Stateful, RoutingMode::Stateful);
90/// assert!(state.is_stateful());
91/// assert!(!state.can_switch_mode());
92///
93/// // Hybrid mode (starts per-command, can switch)
94/// let state = ModeState::new(SessionMode::PerCommand, RoutingMode::Hybrid);
95/// assert!(state.is_per_command());
96/// assert!(state.can_switch_mode());
97///
98/// state.switch_to_stateful();
99/// assert!(state.is_stateful());
100/// // Now permanently stateful for this connection
101/// ```
102#[derive(Debug)]
103pub struct ModeState {
104    /// Current session mode (can change at runtime in Hybrid mode)
105    mode: AtomicU8,
106
107    /// Routing mode configuration (immutable)
108    routing_mode: RoutingMode,
109}
110
111impl ModeState {
112    /// Create a new mode state
113    ///
114    /// # Arguments
115    ///
116    /// * `initial_mode` - Initial session mode
117    /// * `routing_mode` - Routing mode configuration
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// use nntp_proxy::session::{ModeState, SessionMode};
123    /// use nntp_proxy::config::RoutingMode;
124    ///
125    /// let state = ModeState::new(SessionMode::Stateful, RoutingMode::Stateful);
126    /// assert!(state.is_stateful());
127    /// ```
128    #[inline]
129    #[must_use]
130    pub const fn new(initial_mode: SessionMode, routing_mode: RoutingMode) -> Self {
131        Self {
132            mode: AtomicU8::new(initial_mode.to_u8()),
133            routing_mode,
134        }
135    }
136
137    /// Get the current session mode
138    ///
139    /// This is a cheap atomic load operation.
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// use nntp_proxy::session::{ModeState, SessionMode};
145    /// use nntp_proxy::config::RoutingMode;
146    ///
147    /// let state = ModeState::new(SessionMode::PerCommand, RoutingMode::PerCommand);
148    /// assert_eq!(state.mode(), SessionMode::PerCommand);
149    /// ```
150    #[inline]
151    #[must_use]
152    pub fn mode(&self) -> SessionMode {
153        SessionMode::from_u8(self.mode.load(Ordering::Relaxed))
154    }
155
156    /// Get the routing mode configuration
157    ///
158    /// # Examples
159    ///
160    /// ```
161    /// use nntp_proxy::session::{ModeState, SessionMode};
162    /// use nntp_proxy::config::RoutingMode;
163    ///
164    /// let state = ModeState::new(SessionMode::Stateful, RoutingMode::Stateful);
165    /// assert_eq!(state.routing_mode(), RoutingMode::Stateful);
166    /// ```
167    #[inline]
168    #[must_use]
169    pub const fn routing_mode(&self) -> RoutingMode {
170        self.routing_mode
171    }
172
173    /// Check if currently in per-command mode
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// use nntp_proxy::session::{ModeState, SessionMode};
179    /// use nntp_proxy::config::RoutingMode;
180    ///
181    /// let state = ModeState::new(SessionMode::PerCommand, RoutingMode::PerCommand);
182    /// assert!(state.is_per_command());
183    /// ```
184    #[inline]
185    #[must_use]
186    pub fn is_per_command(&self) -> bool {
187        self.mode().is_per_command()
188    }
189
190    /// Check if currently in stateful mode
191    ///
192    /// # Examples
193    ///
194    /// ```
195    /// use nntp_proxy::session::{ModeState, SessionMode};
196    /// use nntp_proxy::config::RoutingMode;
197    ///
198    /// let state = ModeState::new(SessionMode::Stateful, RoutingMode::Stateful);
199    /// assert!(state.is_stateful());
200    /// ```
201    #[inline]
202    #[must_use]
203    pub fn is_stateful(&self) -> bool {
204        self.mode().is_stateful()
205    }
206
207    /// Check if mode switching is allowed
208    ///
209    /// Mode switching is only allowed in Hybrid routing mode.
210    ///
211    /// # Examples
212    ///
213    /// ```
214    /// use nntp_proxy::session::{ModeState, SessionMode};
215    /// use nntp_proxy::config::RoutingMode;
216    ///
217    /// let hybrid = ModeState::new(SessionMode::PerCommand, RoutingMode::Hybrid);
218    /// assert!(hybrid.can_switch_mode());
219    ///
220    /// let stateful = ModeState::new(SessionMode::Stateful, RoutingMode::Stateful);
221    /// assert!(!stateful.can_switch_mode());
222    /// ```
223    #[inline]
224    #[must_use]
225    pub const fn can_switch_mode(&self) -> bool {
226        matches!(self.routing_mode, RoutingMode::Hybrid)
227    }
228
229    /// Switch to stateful mode (one-way transition)
230    ///
231    /// **IMPORTANT**: This is a **permanent, one-way transition** for this connection.
232    /// Once switched from per-command to stateful mode, the connection remains
233    /// stateful for its entire lifetime and **never switches back**.
234    ///
235    /// This transition happens in Hybrid mode when:
236    /// - Client issues a stateful command (GROUP, NEXT, LAST, XOVER, etc.)
237    /// - Client needs server-side state maintained across commands
238    /// - Connection acquires a dedicated backend and keeps it until disconnect
239    ///
240    /// Only allowed in Hybrid routing mode. No-op if already stateful or
241    /// if routing mode doesn't allow switching.
242    ///
243    /// # Examples
244    ///
245    /// ```
246    /// use nntp_proxy::session::{ModeState, SessionMode};
247    /// use nntp_proxy::config::RoutingMode;
248    ///
249    /// let state = ModeState::new(SessionMode::PerCommand, RoutingMode::Hybrid);
250    /// assert!(state.is_per_command());
251    ///
252    /// // Client sends "GROUP alt.binaries.test"
253    /// state.switch_to_stateful();
254    /// assert!(state.is_stateful());
255    ///
256    /// // Connection stays stateful until client disconnects
257    /// // (no way to switch back to per-command)
258    /// ```
259    #[inline]
260    pub fn switch_to_stateful(&self) {
261        if self.can_switch_mode() {
262            self.mode
263                .store(SessionMode::Stateful.to_u8(), Ordering::Relaxed);
264        }
265    }
266
267    /// Check if this session is using per-command routing
268    ///
269    /// Returns true if `routing_mode` is `PerCommand` or Hybrid.
270    ///
271    /// # Examples
272    ///
273    /// ```
274    /// use nntp_proxy::session::{ModeState, SessionMode};
275    /// use nntp_proxy::config::RoutingMode;
276    ///
277    /// let per_cmd = ModeState::new(SessionMode::PerCommand, RoutingMode::PerCommand);
278    /// assert!(per_cmd.is_per_command_routing());
279    ///
280    /// let hybrid = ModeState::new(SessionMode::PerCommand, RoutingMode::Hybrid);
281    /// assert!(hybrid.is_per_command_routing());
282    ///
283    /// let stateful = ModeState::new(SessionMode::Stateful, RoutingMode::Stateful);
284    /// assert!(!stateful.is_per_command_routing());
285    /// ```
286    #[inline]
287    #[must_use]
288    pub const fn is_per_command_routing(&self) -> bool {
289        matches!(
290            self.routing_mode,
291            RoutingMode::PerCommand | RoutingMode::Hybrid
292        )
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn test_session_mode_is_per_command() {
302        assert!(SessionMode::PerCommand.is_per_command());
303        assert!(!SessionMode::Stateful.is_per_command());
304    }
305
306    #[test]
307    fn test_session_mode_is_stateful() {
308        assert!(SessionMode::Stateful.is_stateful());
309        assert!(!SessionMode::PerCommand.is_stateful());
310    }
311
312    #[test]
313    fn test_session_mode_roundtrip() {
314        assert_eq!(
315            SessionMode::from_u8(SessionMode::PerCommand.to_u8()),
316            SessionMode::PerCommand
317        );
318        assert_eq!(
319            SessionMode::from_u8(SessionMode::Stateful.to_u8()),
320            SessionMode::Stateful
321        );
322    }
323
324    #[test]
325    fn test_mode_state_new() {
326        let state = ModeState::new(SessionMode::PerCommand, RoutingMode::PerCommand);
327        assert_eq!(state.mode(), SessionMode::PerCommand);
328        assert_eq!(state.routing_mode(), RoutingMode::PerCommand);
329    }
330
331    #[test]
332    fn test_mode_state_is_per_command() {
333        let state = ModeState::new(SessionMode::PerCommand, RoutingMode::PerCommand);
334        assert!(state.is_per_command());
335        assert!(!state.is_stateful());
336    }
337
338    #[test]
339    fn test_mode_state_is_stateful() {
340        let state = ModeState::new(SessionMode::Stateful, RoutingMode::Stateful);
341        assert!(state.is_stateful());
342        assert!(!state.is_per_command());
343    }
344
345    #[test]
346    fn test_can_switch_mode_hybrid() {
347        let state = ModeState::new(SessionMode::PerCommand, RoutingMode::Hybrid);
348        assert!(state.can_switch_mode());
349    }
350
351    #[test]
352    fn test_cannot_switch_mode_stateful() {
353        let state = ModeState::new(SessionMode::Stateful, RoutingMode::Stateful);
354        assert!(!state.can_switch_mode());
355    }
356
357    #[test]
358    fn test_cannot_switch_mode_per_command() {
359        let state = ModeState::new(SessionMode::PerCommand, RoutingMode::PerCommand);
360        assert!(!state.can_switch_mode());
361    }
362
363    #[test]
364    fn test_switch_to_stateful_in_hybrid() {
365        let state = ModeState::new(SessionMode::PerCommand, RoutingMode::Hybrid);
366        assert!(state.is_per_command());
367
368        state.switch_to_stateful();
369        assert!(state.is_stateful());
370    }
371
372    #[test]
373    fn test_switch_to_stateful_noop_in_stateful_mode() {
374        let state = ModeState::new(SessionMode::Stateful, RoutingMode::Stateful);
375        assert!(state.is_stateful());
376
377        state.switch_to_stateful();
378        assert!(state.is_stateful());
379    }
380
381    #[test]
382    fn test_switch_to_stateful_noop_in_per_command_mode() {
383        let state = ModeState::new(SessionMode::PerCommand, RoutingMode::PerCommand);
384        assert!(state.is_per_command());
385
386        state.switch_to_stateful();
387        // Should remain in per-command mode
388        assert!(state.is_per_command());
389    }
390
391    #[test]
392    fn test_is_per_command_routing() {
393        let per_cmd = ModeState::new(SessionMode::PerCommand, RoutingMode::PerCommand);
394        assert!(per_cmd.is_per_command_routing());
395
396        let hybrid = ModeState::new(SessionMode::PerCommand, RoutingMode::Hybrid);
397        assert!(hybrid.is_per_command_routing());
398
399        let stateful = ModeState::new(SessionMode::Stateful, RoutingMode::Stateful);
400        assert!(!stateful.is_per_command_routing());
401    }
402
403    #[test]
404    fn test_one_way_transition_invariant() {
405        // Once switched to stateful in hybrid mode, stays stateful forever
406        let state = ModeState::new(SessionMode::PerCommand, RoutingMode::Hybrid);
407        assert!(state.is_per_command());
408
409        // Simulate client sending "GROUP alt.test"
410        state.switch_to_stateful();
411        assert!(state.is_stateful());
412
413        // No way to switch back - would need new connection
414        // (No switch_to_per_command() method exists)
415
416        // Verify it stays stateful
417        assert!(state.is_stateful());
418        assert!(!state.is_per_command());
419
420        // Can call switch_to_stateful again (no-op)
421        state.switch_to_stateful();
422        assert!(state.is_stateful());
423    }
424}