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}