Skip to main content

nautilus_network/
mode.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Connection mode enumeration for socket clients.
17
18use std::sync::{
19    Arc,
20    atomic::{AtomicBool, AtomicU8, Ordering},
21};
22
23use strum::{AsRefStr, Display, EnumString};
24
25/// Irreversible validity token for a single connection's read task.
26#[derive(Clone, Debug)]
27pub(crate) struct ReadSessionFence {
28    valid: Arc<AtomicBool>,
29}
30
31impl ReadSessionFence {
32    /// Creates a valid fence for a newly spawned read task.
33    #[must_use]
34    pub(crate) fn new() -> Self {
35        Self {
36            valid: Arc::new(AtomicBool::new(true)),
37        }
38    }
39
40    /// Invalidates the associated read session.
41    pub(crate) fn invalidate(&self) {
42        self.valid.store(false, Ordering::SeqCst);
43    }
44
45    /// Returns whether the associated read session is still current.
46    #[must_use]
47    pub(crate) fn is_valid(&self) -> bool {
48        self.valid.load(Ordering::SeqCst)
49    }
50}
51
52/// Connection mode for a socket client.
53///
54/// The client can be in one of four modes (managed via an atomic flag).
55#[derive(Clone, Copy, Debug, Default, Display, Hash, PartialEq, Eq, AsRefStr, EnumString)]
56#[repr(u8)]
57#[strum(serialize_all = "UPPERCASE")]
58pub enum ConnectionMode {
59    #[default]
60    /// The client is fully connected and operational.
61    /// All tasks (reading, writing, heartbeat) are running normally.
62    Active = 0,
63    /// The client has been disconnected or has been explicitly signaled to reconnect.
64    /// In this state, active tasks are paused until a new connection is established.
65    Reconnect = 1,
66    /// The client has been explicitly signaled to disconnect.
67    /// No further reconnection attempts will be made, and cleanup procedures are initiated.
68    Disconnect = 2,
69    /// The client is permanently closed.
70    /// All associated tasks have been terminated and the connection is no longer available.
71    Closed = 3,
72}
73
74impl ConnectionMode {
75    /// Convert a u8 to [`ConnectionMode`], useful when loading from an `AtomicU8`.
76    ///
77    /// # Panics
78    ///
79    /// Panics if `value` is not a valid `ConnectionMode` discriminant (must be between 0 and 3 inclusive).
80    #[inline]
81    #[must_use]
82    pub fn from_u8(value: u8) -> Self {
83        match value {
84            0 => Self::Active,
85            1 => Self::Reconnect,
86            2 => Self::Disconnect,
87            3 => Self::Closed,
88            _ => panic!("Invalid `ConnectionMode` value: {value}"),
89        }
90    }
91
92    /// Load a [`ConnectionMode`] from an [`AtomicU8`] using sequential consistency ordering.
93    #[inline]
94    #[must_use]
95    pub fn from_atomic(value: &AtomicU8) -> Self {
96        Self::from_u8(value.load(Ordering::SeqCst))
97    }
98
99    /// Atomically transitions to `Reconnect`, but only from `Active`.
100    ///
101    /// Returns `true` if this call performed the transition. A concurrent
102    /// `Disconnect`/`Closed` (or an in-flight `Reconnect`) is left untouched,
103    /// so a writer detecting a dead connection cannot resurrect a client that
104    /// is being torn down.
105    pub fn request_reconnect(value: &AtomicU8) -> bool {
106        value
107            .compare_exchange(
108                Self::Active.as_u8(),
109                Self::Reconnect.as_u8(),
110                Ordering::SeqCst,
111                Ordering::SeqCst,
112            )
113            .is_ok()
114    }
115
116    /// Atomically transitions to `Disconnect` from any non-`Closed` state.
117    ///
118    /// Returns `true` if the mode is now `Disconnect`; `false` if the
119    /// connection was already `Closed` (terminal state is preserved so status
120    /// queries keep reporting `Closed`).
121    pub fn request_disconnect(value: &AtomicU8) -> bool {
122        value
123            .try_update(Ordering::SeqCst, Ordering::SeqCst, |mode| {
124                (!Self::from_u8(mode).is_closed()).then_some(Self::Disconnect.as_u8())
125            })
126            .is_ok()
127    }
128
129    /// Convert a [`ConnectionMode`] to a u8, useful when storing to an `AtomicU8`.
130    #[inline]
131    #[must_use]
132    pub const fn as_u8(self) -> u8 {
133        self as u8
134    }
135
136    /// Returns true if the client is in an active state.
137    #[inline]
138    #[must_use]
139    pub const fn is_active(&self) -> bool {
140        matches!(self, Self::Active)
141    }
142
143    /// Returns true if the client is attempting to reconnect.
144    #[inline]
145    #[must_use]
146    pub const fn is_reconnect(&self) -> bool {
147        matches!(self, Self::Reconnect)
148    }
149
150    /// Returns true if the client is attempting to disconnect.
151    #[inline]
152    #[must_use]
153    pub const fn is_disconnect(&self) -> bool {
154        matches!(self, Self::Disconnect)
155    }
156
157    /// Returns true if the client connection is closed.
158    #[inline]
159    #[must_use]
160    pub const fn is_closed(&self) -> bool {
161        matches!(self, Self::Closed)
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use rstest::rstest;
168
169    use super::*;
170
171    #[rstest]
172    #[case(ConnectionMode::Active, true, ConnectionMode::Reconnect)]
173    #[case(ConnectionMode::Reconnect, false, ConnectionMode::Reconnect)]
174    #[case(ConnectionMode::Disconnect, false, ConnectionMode::Disconnect)]
175    #[case(ConnectionMode::Closed, false, ConnectionMode::Closed)]
176    fn request_reconnect_transitions(
177        #[case] start: ConnectionMode,
178        #[case] expected_result: bool,
179        #[case] expected_mode: ConnectionMode,
180    ) {
181        let mode = AtomicU8::new(start.as_u8());
182
183        assert_eq!(ConnectionMode::request_reconnect(&mode), expected_result);
184        assert_eq!(ConnectionMode::from_atomic(&mode), expected_mode);
185    }
186
187    #[rstest]
188    #[case(ConnectionMode::Active, true, ConnectionMode::Disconnect)]
189    #[case(ConnectionMode::Reconnect, true, ConnectionMode::Disconnect)]
190    #[case(ConnectionMode::Disconnect, true, ConnectionMode::Disconnect)]
191    #[case(ConnectionMode::Closed, false, ConnectionMode::Closed)]
192    fn request_disconnect_transitions(
193        #[case] start: ConnectionMode,
194        #[case] expected_result: bool,
195        #[case] expected_mode: ConnectionMode,
196    ) {
197        let mode = AtomicU8::new(start.as_u8());
198
199        assert_eq!(ConnectionMode::request_disconnect(&mode), expected_result);
200        assert_eq!(ConnectionMode::from_atomic(&mode), expected_mode);
201    }
202}