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}