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}