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::atomic::{AtomicU8, Ordering};
19
20use strum::{AsRefStr, Display, EnumString};
21
22/// Connection mode for a socket client.
23///
24/// The client can be in one of four modes (managed via an atomic flag).
25#[derive(Clone, Copy, Debug, Default, Display, Hash, PartialEq, Eq, AsRefStr, EnumString)]
26#[repr(u8)]
27#[strum(serialize_all = "UPPERCASE")]
28pub enum ConnectionMode {
29    #[default]
30    /// The client is fully connected and operational.
31    /// All tasks (reading, writing, heartbeat) are running normally.
32    Active = 0,
33    /// The client has been disconnected or has been explicitly signaled to reconnect.
34    /// In this state, active tasks are paused until a new connection is established.
35    Reconnect = 1,
36    /// The client has been explicitly signaled to disconnect.
37    /// No further reconnection attempts will be made, and cleanup procedures are initiated.
38    Disconnect = 2,
39    /// The client is permanently closed.
40    /// All associated tasks have been terminated and the connection is no longer available.
41    Closed = 3,
42}
43
44impl ConnectionMode {
45    /// Convert a u8 to [`ConnectionMode`], useful when loading from an `AtomicU8`.
46    ///
47    /// # Panics
48    ///
49    /// Panics if `value` is not a valid `ConnectionMode` discriminant (must be between 0 and 3 inclusive).
50    #[inline]
51    #[must_use]
52    pub fn from_u8(value: u8) -> Self {
53        match value {
54            0 => Self::Active,
55            1 => Self::Reconnect,
56            2 => Self::Disconnect,
57            3 => Self::Closed,
58            _ => panic!("Invalid `ConnectionMode` value: {value}"),
59        }
60    }
61
62    /// Load a [`ConnectionMode`] from an [`AtomicU8`] using sequential consistency ordering.
63    #[inline]
64    #[must_use]
65    pub fn from_atomic(value: &AtomicU8) -> Self {
66        Self::from_u8(value.load(Ordering::SeqCst))
67    }
68
69    /// Atomically transitions to `Reconnect`, but only from `Active`.
70    ///
71    /// Returns `true` if this call performed the transition. A concurrent
72    /// `Disconnect`/`Closed` (or an in-flight `Reconnect`) is left untouched,
73    /// so a writer detecting a dead connection cannot resurrect a client that
74    /// is being torn down.
75    pub fn request_reconnect(value: &AtomicU8) -> bool {
76        value
77            .compare_exchange(
78                Self::Active.as_u8(),
79                Self::Reconnect.as_u8(),
80                Ordering::SeqCst,
81                Ordering::SeqCst,
82            )
83            .is_ok()
84    }
85
86    /// Atomically transitions to `Disconnect` from any non-`Closed` state.
87    ///
88    /// Returns `true` if the mode is now `Disconnect`; `false` if the
89    /// connection was already `Closed` (terminal state is preserved so status
90    /// queries keep reporting `Closed`).
91    pub fn request_disconnect(value: &AtomicU8) -> bool {
92        value
93            .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |mode| {
94                (!Self::from_u8(mode).is_closed()).then_some(Self::Disconnect.as_u8())
95            })
96            .is_ok()
97    }
98
99    /// Convert a [`ConnectionMode`] to a u8, useful when storing to an `AtomicU8`.
100    #[inline]
101    #[must_use]
102    pub const fn as_u8(self) -> u8 {
103        self as u8
104    }
105
106    /// Returns true if the client is in an active state.
107    #[inline]
108    #[must_use]
109    pub const fn is_active(&self) -> bool {
110        matches!(self, Self::Active)
111    }
112
113    /// Returns true if the client is attempting to reconnect.
114    #[inline]
115    #[must_use]
116    pub const fn is_reconnect(&self) -> bool {
117        matches!(self, Self::Reconnect)
118    }
119
120    /// Returns true if the client is attempting to disconnect.
121    #[inline]
122    #[must_use]
123    pub const fn is_disconnect(&self) -> bool {
124        matches!(self, Self::Disconnect)
125    }
126
127    /// Returns true if the client connection is closed.
128    #[inline]
129    #[must_use]
130    pub const fn is_closed(&self) -> bool {
131        matches!(self, Self::Closed)
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use rstest::rstest;
138
139    use super::*;
140
141    #[rstest]
142    #[case(ConnectionMode::Active, true, ConnectionMode::Reconnect)]
143    #[case(ConnectionMode::Reconnect, false, ConnectionMode::Reconnect)]
144    #[case(ConnectionMode::Disconnect, false, ConnectionMode::Disconnect)]
145    #[case(ConnectionMode::Closed, false, ConnectionMode::Closed)]
146    fn request_reconnect_transitions(
147        #[case] start: ConnectionMode,
148        #[case] expected_result: bool,
149        #[case] expected_mode: ConnectionMode,
150    ) {
151        let mode = AtomicU8::new(start.as_u8());
152
153        assert_eq!(ConnectionMode::request_reconnect(&mode), expected_result);
154        assert_eq!(ConnectionMode::from_atomic(&mode), expected_mode);
155    }
156
157    #[rstest]
158    #[case(ConnectionMode::Active, true, ConnectionMode::Disconnect)]
159    #[case(ConnectionMode::Reconnect, true, ConnectionMode::Disconnect)]
160    #[case(ConnectionMode::Disconnect, true, ConnectionMode::Disconnect)]
161    #[case(ConnectionMode::Closed, false, ConnectionMode::Closed)]
162    fn request_disconnect_transitions(
163        #[case] start: ConnectionMode,
164        #[case] expected_result: bool,
165        #[case] expected_mode: ConnectionMode,
166    ) {
167        let mode = AtomicU8::new(start.as_u8());
168
169        assert_eq!(ConnectionMode::request_disconnect(&mode), expected_result);
170        assert_eq!(ConnectionMode::from_atomic(&mode), expected_mode);
171    }
172}