1use std::sync::{
33 Arc, OnceLock,
34 atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering},
35};
36
37use strum::{AsRefStr, Display, EnumString};
38
39use crate::sink::{SocketState, SocketStateSink};
40
41#[derive(Clone, Copy, Debug, Default, Display, Hash, PartialEq, Eq, AsRefStr, EnumString)]
46#[repr(u8)]
47#[strum(serialize_all = "UPPERCASE")]
48pub enum ConnectionMode {
49 #[default]
50 Active = 0,
53 Reconnect = 1,
56 Disconnect = 2,
59 Closed = 3,
62}
63
64impl ConnectionMode {
65 #[inline]
71 #[must_use]
72 pub fn from_u8(value: u8) -> Self {
73 match value {
74 0 => Self::Active,
75 1 => Self::Reconnect,
76 2 => Self::Disconnect,
77 3 => Self::Closed,
78 _ => panic!("Invalid `ConnectionMode` value: {value}"),
79 }
80 }
81
82 #[inline]
84 #[must_use]
85 pub fn from_atomic(value: &AtomicU8) -> Self {
86 Self::from_u8(value.load(Ordering::SeqCst))
87 }
88
89 pub fn request_reconnect(value: &AtomicU8) -> bool {
96 Self::request_reconnect_outcome(value) == ReconnectRequestOutcome::Accepted
97 }
98
99 pub(crate) fn request_reconnect_outcome(value: &AtomicU8) -> ReconnectRequestOutcome {
101 match value.compare_exchange(
102 Self::Active.as_u8(),
103 Self::Reconnect.as_u8(),
104 Ordering::SeqCst,
105 Ordering::SeqCst,
106 ) {
107 Ok(_) => ReconnectRequestOutcome::Accepted,
108 Err(actual) => ReconnectRequestOutcome::from_rejected(Self::from_u8(actual)),
109 }
110 }
111
112 pub(crate) fn request_reconnect_with_sink(
114 value: &AtomicU8,
115 sink: Option<&SocketStateSink>,
116 ) -> bool {
117 Self::request_reconnect_outcome_with_sink(value, sink) == ReconnectRequestOutcome::Accepted
118 }
119
120 pub(crate) fn request_reconnect_outcome_with_sink(
121 value: &AtomicU8,
122 sink: Option<&SocketStateSink>,
123 ) -> ReconnectRequestOutcome {
124 sink.map_or_else(
125 || Self::request_reconnect_outcome(value),
126 |sink| {
127 sink.transition_result(
128 value,
129 Self::Active,
130 Self::Reconnect,
131 SocketState::Disconnected,
132 )
133 .map_or_else(ReconnectRequestOutcome::from_rejected, |()| {
134 ReconnectRequestOutcome::Accepted
135 })
136 },
137 )
138 }
139
140 pub(crate) fn close_websocket_on_loss(
143 value: &AtomicU8,
144 sink: Option<&SocketStateSink>,
145 ) -> bool {
146 sink.map_or_else(
147 || {
148 value
149 .try_update(Ordering::SeqCst, Ordering::SeqCst, |mode| {
150 matches!(Self::from_u8(mode), Self::Active | Self::Reconnect)
151 .then_some(Self::Closed.as_u8())
152 })
153 .is_ok()
154 },
155 |sink| sink.close_on_loss(value),
156 )
157 }
158
159 pub fn request_disconnect(value: &AtomicU8) -> bool {
165 value
166 .try_update(Ordering::SeqCst, Ordering::SeqCst, |mode| {
167 (!Self::from_u8(mode).is_closed()).then_some(Self::Disconnect.as_u8())
168 })
169 .is_ok()
170 }
171
172 pub(crate) fn complete_reconnect(value: &AtomicU8) -> ReconnectOutcome {
181 if value
182 .compare_exchange(
183 Self::Reconnect.as_u8(),
184 Self::Active.as_u8(),
185 Ordering::SeqCst,
186 Ordering::SeqCst,
187 )
188 .is_ok()
189 {
190 ReconnectOutcome::Reconnected
191 } else {
192 ReconnectOutcome::Aborted
193 }
194 }
195
196 pub(crate) fn complete_reconnect_with_sink(
198 value: &AtomicU8,
199 sink: Option<&SocketStateSink>,
200 ) -> ReconnectOutcome {
201 let reconnected = sink.map_or_else(
202 || Self::complete_reconnect(value) == ReconnectOutcome::Reconnected,
203 |sink| sink.transition(value, Self::Reconnect, Self::Active, SocketState::Connected),
204 );
205
206 if reconnected {
207 ReconnectOutcome::Reconnected
208 } else {
209 ReconnectOutcome::Aborted
210 }
211 }
212
213 #[inline]
215 #[must_use]
216 pub const fn as_u8(self) -> u8 {
217 self as u8
218 }
219
220 #[inline]
222 #[must_use]
223 pub const fn is_active(&self) -> bool {
224 matches!(self, Self::Active)
225 }
226
227 #[inline]
229 #[must_use]
230 pub const fn is_reconnect(&self) -> bool {
231 matches!(self, Self::Reconnect)
232 }
233
234 #[inline]
236 #[must_use]
237 pub const fn is_disconnect(&self) -> bool {
238 matches!(self, Self::Disconnect)
239 }
240
241 #[inline]
243 #[must_use]
244 pub const fn is_closed(&self) -> bool {
245 matches!(self, Self::Closed)
246 }
247}
248
249#[derive(Clone, Copy, Debug, Eq, PartialEq)]
251pub enum ReconnectRequestOutcome {
252 Accepted,
254 AlreadyReconnecting,
256 Disconnected,
258 Closed,
260 Unsupported,
262}
263
264impl ReconnectRequestOutcome {
265 fn from_rejected(mode: ConnectionMode) -> Self {
266 match mode {
267 ConnectionMode::Active | ConnectionMode::Reconnect => Self::AlreadyReconnecting,
268 ConnectionMode::Disconnect => Self::Disconnected,
269 ConnectionMode::Closed => Self::Closed,
270 }
271 }
272}
273
274#[derive(Clone, Copy, Debug, Eq, PartialEq)]
281pub(crate) enum ReconnectOutcome {
282 Reconnected,
284 Aborted,
286}
287
288#[derive(Clone, Debug)]
290pub(crate) struct ReadSessionFence {
291 valid: Arc<AtomicBool>,
292}
293
294impl ReadSessionFence {
295 #[must_use]
297 pub(crate) fn new() -> Self {
298 Self {
299 valid: Arc::new(AtomicBool::new(true)),
300 }
301 }
302
303 pub(crate) fn invalidate(&self) {
305 self.valid.store(false, Ordering::SeqCst);
306 }
307
308 #[must_use]
310 pub(crate) fn is_valid(&self) -> bool {
311 self.valid.load(Ordering::SeqCst)
312 }
313}
314
315const CONTROLLER_CLOSED: usize = 1 << (usize::BITS - 1);
316const CONTROLLER_REQUEST_MASK: usize = CONTROLLER_CLOSED - 1;
317
318pub(crate) struct ControllerLifecycle {
319 state: AtomicUsize,
320 abort_handle: OnceLock<tokio::task::AbortHandle>,
321}
322
323impl ControllerLifecycle {
324 pub(crate) const fn new() -> Self {
325 Self {
326 state: AtomicUsize::new(0),
327 abort_handle: OnceLock::new(),
328 }
329 }
330
331 pub(crate) fn enter_request(&self) -> Option<ControllerRequest<'_>> {
332 self.state
333 .try_update(Ordering::SeqCst, Ordering::SeqCst, |state| {
334 if state & CONTROLLER_CLOSED != 0 {
335 None
336 } else {
337 assert_ne!(
338 state, CONTROLLER_REQUEST_MASK,
339 "too many reconnect requests"
340 );
341 Some(state + 1)
342 }
343 })
344 .ok()
345 .map(|_| ControllerRequest(self))
346 }
347
348 pub(crate) fn set_abort_handle(&self, abort_handle: tokio::task::AbortHandle) {
349 assert!(
350 self.abort_handle.set(abort_handle).is_ok(),
351 "controller abort handle already set"
352 );
353 }
354
355 pub(crate) fn close_and_abort(&self) {
356 let previous = self.state.fetch_or(CONTROLLER_CLOSED, Ordering::SeqCst);
357 if previous & CONTROLLER_REQUEST_MASK == 0 {
358 self.abort();
359 }
360 }
361
362 pub(crate) fn activity(self: &Arc<Self>) -> ControllerActivity {
363 ControllerActivity(Arc::clone(self))
364 }
365
366 fn close(&self) {
367 self.state.fetch_or(CONTROLLER_CLOSED, Ordering::SeqCst);
368 }
369
370 fn abort(&self) {
371 if let Some(abort_handle) = self.abort_handle.get() {
372 abort_handle.abort();
373 }
374 }
375}
376
377pub(crate) struct ControllerRequest<'a>(&'a ControllerLifecycle);
378
379impl Drop for ControllerRequest<'_> {
380 fn drop(&mut self) {
381 let previous = self.0.state.fetch_sub(1, Ordering::SeqCst);
382 if previous == CONTROLLER_CLOSED | 1 {
383 self.0.abort();
384 }
385 }
386}
387
388pub(crate) struct ControllerActivity(Arc<ControllerLifecycle>);
389
390impl Drop for ControllerActivity {
391 fn drop(&mut self) {
392 self.0.close();
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use rstest::rstest;
399
400 use super::*;
401
402 #[rstest]
403 #[case(ConnectionMode::Active, true, ConnectionMode::Reconnect)]
404 #[case(ConnectionMode::Reconnect, false, ConnectionMode::Reconnect)]
405 #[case(ConnectionMode::Disconnect, false, ConnectionMode::Disconnect)]
406 #[case(ConnectionMode::Closed, false, ConnectionMode::Closed)]
407 fn request_reconnect_transitions(
408 #[case] start: ConnectionMode,
409 #[case] expected_result: bool,
410 #[case] expected_mode: ConnectionMode,
411 ) {
412 let mode = AtomicU8::new(start.as_u8());
413
414 assert_eq!(ConnectionMode::request_reconnect(&mode), expected_result);
415 assert_eq!(ConnectionMode::from_atomic(&mode), expected_mode);
416 }
417
418 #[rstest]
422 #[case(
423 ConnectionMode::Reconnect,
424 ReconnectOutcome::Reconnected,
425 ConnectionMode::Active
426 )]
427 #[case(
428 ConnectionMode::Disconnect,
429 ReconnectOutcome::Aborted,
430 ConnectionMode::Disconnect
431 )]
432 #[case(
433 ConnectionMode::Closed,
434 ReconnectOutcome::Aborted,
435 ConnectionMode::Closed
436 )]
437 #[case(
438 ConnectionMode::Active,
439 ReconnectOutcome::Aborted,
440 ConnectionMode::Active
441 )]
442 fn complete_reconnect_transitions(
443 #[case] start: ConnectionMode,
444 #[case] expected_outcome: ReconnectOutcome,
445 #[case] expected_mode: ConnectionMode,
446 ) {
447 let mode = AtomicU8::new(start.as_u8());
448
449 assert_eq!(ConnectionMode::complete_reconnect(&mode), expected_outcome);
450 assert_eq!(ConnectionMode::from_atomic(&mode), expected_mode);
451 }
452
453 #[rstest]
454 #[case(ConnectionMode::Active, true, ConnectionMode::Disconnect)]
455 #[case(ConnectionMode::Reconnect, true, ConnectionMode::Disconnect)]
456 #[case(ConnectionMode::Disconnect, true, ConnectionMode::Disconnect)]
457 #[case(ConnectionMode::Closed, false, ConnectionMode::Closed)]
458 fn request_disconnect_transitions(
459 #[case] start: ConnectionMode,
460 #[case] expected_result: bool,
461 #[case] expected_mode: ConnectionMode,
462 ) {
463 let mode = AtomicU8::new(start.as_u8());
464
465 assert_eq!(ConnectionMode::request_disconnect(&mode), expected_result);
466 assert_eq!(ConnectionMode::from_atomic(&mode), expected_mode);
467 }
468}