Skip to main content

nautilus_live/
socket.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//! Socket state publication and reconnect control for live clients.
17
18use std::{
19    cell::RefCell,
20    fmt::Debug,
21    sync::{
22        Arc, Weak,
23        atomic::{AtomicU64, Ordering},
24    },
25};
26
27use ahash::{AHashMap, AHashSet};
28use nautilus_common::{
29    live::{runner::try_get_system_event_sender, sender::EventSender},
30    messages::{
31        SystemEvent,
32        system::{SocketState as SystemSocketState, SocketStateChange},
33    },
34};
35use nautilus_model::identifiers::{ClientId, Venue};
36pub use nautilus_network::mode::ReconnectRequestOutcome as SocketReconnectRequestOutcome;
37use nautilus_network::{SocketState, SocketStateSink, mode::ReconnectRequestOutcome};
38use parking_lot::Mutex;
39use ustr::Ustr;
40
41thread_local! {
42    static SOCKET_REGISTRARS: RefCell<Vec<SocketReconnectRegistrar>> = const { RefCell::new(Vec::new()) };
43}
44
45/// Outcome from resolving a client socket through a live node registry.
46#[derive(Clone, Debug)]
47pub enum SocketReconnectLookup {
48    /// No client with the requested ID belongs to the live node.
49    ClientNotFound,
50    /// The client has no controller-reconnectable sockets.
51    Unsupported,
52    /// The client supports socket reconnects but not for the requested endpoint.
53    EndpointNotFound,
54    /// More than one client surface owns the requested endpoint.
55    AmbiguousEndpoint,
56    /// The endpoint resolved to one reconnect handle.
57    Handle(SocketReconnectHandle),
58}
59
60/// Cloneable control handle for one registered socket endpoint.
61#[derive(Clone)]
62pub struct SocketReconnectHandle {
63    request: Arc<dyn Fn() -> ReconnectRequestOutcome + Send + Sync>,
64}
65
66impl SocketReconnectHandle {
67    fn new<F>(request: F) -> Self
68    where
69        F: Fn() -> ReconnectRequestOutcome + Send + Sync + 'static,
70    {
71        Self {
72            request: Arc::new(request),
73        }
74    }
75
76    /// Requests reconnect of the registered endpoint.
77    #[must_use]
78    pub fn request_reconnect(&self) -> ReconnectRequestOutcome {
79        (self.request)()
80    }
81}
82
83impl Debug for SocketReconnectHandle {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        f.debug_struct(stringify!(SocketReconnectHandle))
86            .finish_non_exhaustive()
87    }
88}
89
90#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
91struct SocketEndpoint {
92    client_id: ClientId,
93    endpoint: Ustr,
94}
95
96#[derive(Debug)]
97struct RegistryEntry {
98    generation: u64,
99    handle: SocketReconnectHandle,
100    request: Arc<Mutex<Option<SocketReconnectHandle>>>,
101}
102
103#[derive(Debug, Default)]
104struct RegistryInner {
105    clients: AHashSet<ClientId>,
106    supported: AHashSet<ClientId>,
107    entries: AHashMap<SocketEndpoint, AHashMap<u64, RegistryEntry>>,
108    owners: AHashMap<ClientId, AHashSet<u64>>,
109    next_generation: u64,
110    next_owner: u64,
111}
112
113/// Registry of reconnectable socket endpoints owned by one live node.
114#[derive(Debug, Default)]
115pub struct SocketReconnectRegistry {
116    inner: Arc<Mutex<RegistryInner>>,
117}
118
119impl SocketReconnectRegistry {
120    /// Makes this registry available while live client factories construct their clients.
121    pub fn scope<T>(&self, f: impl FnOnce() -> T) -> T {
122        SOCKET_REGISTRARS.with(|registrars| {
123            registrars.borrow_mut().push(self.registrar());
124        });
125
126        let _scope = SocketRegistryScope;
127        f()
128    }
129
130    #[cfg(any(feature = "node", test))]
131    pub(crate) fn register_client(&self, client_id: ClientId) {
132        self.inner.lock().clients.insert(client_id);
133    }
134
135    /// Resolves one logical socket endpoint.
136    #[must_use]
137    pub fn get(&self, client_id: ClientId, endpoint: Ustr) -> SocketReconnectLookup {
138        let inner = self.inner.lock();
139
140        let key = SocketEndpoint {
141            client_id,
142            endpoint,
143        };
144
145        match inner.entries.get(&key) {
146            Some(entries) if entries.len() > 1 => SocketReconnectLookup::AmbiguousEndpoint,
147            Some(entries) => entries
148                .values()
149                .next()
150                .map_or(SocketReconnectLookup::EndpointNotFound, |entry| {
151                    SocketReconnectLookup::Handle(entry.handle.clone())
152                }),
153            None if inner.supported.contains(&client_id) => SocketReconnectLookup::EndpointNotFound,
154            None if inner.clients.contains(&client_id) => SocketReconnectLookup::Unsupported,
155            None => SocketReconnectLookup::ClientNotFound,
156        }
157    }
158
159    /// Returns the reconnect handle when exactly one owner registered `endpoint`.
160    #[must_use]
161    pub fn handle(&self, client_id: ClientId, endpoint: Ustr) -> Option<SocketReconnectHandle> {
162        match self.get(client_id, endpoint) {
163            SocketReconnectLookup::Handle(handle) => Some(handle),
164            _ => None,
165        }
166    }
167
168    fn registrar(&self) -> SocketReconnectRegistrar {
169        SocketReconnectRegistrar {
170            registry: Arc::downgrade(&self.inner),
171        }
172    }
173}
174
175struct SocketRegistryScope;
176
177impl Drop for SocketRegistryScope {
178    fn drop(&mut self) {
179        SOCKET_REGISTRARS.with(|registrars| {
180            registrars.borrow_mut().pop();
181        });
182    }
183}
184
185#[derive(Clone, Debug)]
186struct SocketReconnectRegistrar {
187    registry: Weak<Mutex<RegistryInner>>,
188}
189
190impl SocketReconnectRegistrar {
191    fn owner(&self, client_id: ClientId) -> Option<SocketReconnectOwner> {
192        let registry = self.registry.upgrade()?;
193
194        let owner_id = {
195            let mut inner = registry.lock();
196            inner.next_owner = inner.next_owner.wrapping_add(1).max(1);
197            let owner_id = inner.next_owner;
198            inner.supported.insert(client_id);
199            inner.owners.entry(client_id).or_default().insert(owner_id);
200            owner_id
201        };
202
203        Some(SocketReconnectOwner(Arc::new(SocketReconnectOwnerInner {
204            registry: Arc::downgrade(&registry),
205            client_id,
206            owner_id,
207        })))
208    }
209}
210
211#[derive(Clone, Debug)]
212struct SocketReconnectOwner(Arc<SocketReconnectOwnerInner>);
213
214impl SocketReconnectOwner {
215    fn register(
216        &self,
217        endpoint: Ustr,
218        handle: SocketReconnectHandle,
219    ) -> (Option<SocketReconnectRegistration>, Option<RegistryEntry>) {
220        let Some(registry) = self.0.registry.upgrade() else {
221            return (None, None);
222        };
223
224        let key = SocketEndpoint {
225            client_id: self.0.client_id,
226            endpoint,
227        };
228
229        let replaced = remove_entry(&registry, key, self.0.owner_id, None);
230
231        let request = Arc::new(Mutex::new(Some(handle)));
232        let guarded_request = Arc::clone(&request);
233        let registry_ref = Arc::downgrade(&registry);
234
235        let guarded_handle = SocketReconnectHandle::new(move || {
236            let Some(_registry) = registry_ref.upgrade() else {
237                return ReconnectRequestOutcome::Closed;
238            };
239
240            let request = guarded_request.lock();
241            request
242                .as_ref()
243                .map_or(ReconnectRequestOutcome::Closed, |handle| {
244                    handle.request_reconnect()
245                })
246        });
247
248        let generation = {
249            let mut inner = registry.lock();
250            inner.next_generation = inner.next_generation.wrapping_add(1).max(1);
251            let generation = inner.next_generation;
252            inner.entries.entry(key).or_default().insert(
253                self.0.owner_id,
254                RegistryEntry {
255                    generation,
256                    handle: guarded_handle,
257                    request,
258                },
259            );
260
261            generation
262        };
263
264        (
265            Some(SocketReconnectRegistration {
266                registry: Arc::downgrade(&registry),
267                key,
268                owner_id: self.0.owner_id,
269                generation,
270            }),
271            replaced,
272        )
273    }
274
275    fn remove(&self, endpoint: Ustr) -> Option<RegistryEntry> {
276        let registry = self.0.registry.upgrade()?;
277
278        let key = SocketEndpoint {
279            client_id: self.0.client_id,
280            endpoint,
281        };
282
283        remove_entry(&registry, key, self.0.owner_id, None)
284    }
285}
286
287#[derive(Debug)]
288struct SocketReconnectOwnerInner {
289    registry: Weak<Mutex<RegistryInner>>,
290    client_id: ClientId,
291    owner_id: u64,
292}
293
294impl Drop for SocketReconnectOwnerInner {
295    fn drop(&mut self) {
296        let Some(registry) = self.registry.upgrade() else {
297            return;
298        };
299
300        let removed = {
301            let mut inner = registry.lock();
302            if let Some(owners) = inner.owners.get_mut(&self.client_id) {
303                owners.remove(&self.owner_id);
304                if owners.is_empty() {
305                    inner.owners.remove(&self.client_id);
306                }
307            }
308
309            let mut removed = Vec::new();
310            inner.entries.retain(|key, entries| {
311                if key.client_id == self.client_id
312                    && let Some(entry) = entries.remove(&self.owner_id)
313                {
314                    removed.push(entry);
315                }
316
317                !entries.is_empty()
318            });
319
320            removed
321        };
322
323        for entry in removed {
324            deactivate(Some(entry));
325        }
326    }
327}
328
329#[derive(Debug)]
330struct SocketReconnectRegistration {
331    registry: Weak<Mutex<RegistryInner>>,
332    key: SocketEndpoint,
333    owner_id: u64,
334    generation: u64,
335}
336
337impl Drop for SocketReconnectRegistration {
338    fn drop(&mut self) {
339        let Some(registry) = self.registry.upgrade() else {
340            return;
341        };
342
343        let entry = remove_entry(&registry, self.key, self.owner_id, Some(self.generation));
344        deactivate(entry);
345    }
346}
347
348fn remove_entry(
349    registry: &Arc<Mutex<RegistryInner>>,
350    key: SocketEndpoint,
351    owner_id: u64,
352    generation: Option<u64>,
353) -> Option<RegistryEntry> {
354    let mut inner = registry.lock();
355    let mut entry = None;
356    let mut remove_endpoint = false;
357
358    if let Some(entries) = inner.entries.get_mut(&key) {
359        let matches = entries
360            .get(&owner_id)
361            .is_some_and(|entry| generation.is_none_or(|value| entry.generation == value));
362
363        if matches {
364            entry = entries.remove(&owner_id);
365        }
366
367        remove_endpoint = entries.is_empty();
368    }
369
370    if remove_endpoint {
371        inner.entries.remove(&key);
372    }
373
374    entry
375}
376
377fn deactivate(entry: Option<RegistryEntry>) {
378    if let Some(entry) = entry {
379        *entry.request.lock() = None;
380    }
381}
382
383/// Creates socket controls which share client identity and registry ownership.
384#[derive(Clone, Debug)]
385pub struct SocketControlFactory {
386    client_id: ClientId,
387    venue: Option<Venue>,
388    sender: Option<EventSender<SystemEvent>>,
389    owner: Option<SocketReconnectOwner>,
390    controls: Arc<Mutex<AHashMap<Ustr, SocketControl>>>,
391}
392
393impl SocketControlFactory {
394    /// Creates a new [`SocketControlFactory`] instance.
395    #[must_use]
396    pub fn new(client_id: ClientId, venue: Option<Venue>) -> Self {
397        let registrar = SOCKET_REGISTRARS.with(|registrars| registrars.borrow().last().cloned());
398        Self::from_registrar(client_id, venue, registrar)
399    }
400
401    /// Creates a factory which registers with an explicit live socket registry.
402    #[must_use]
403    pub fn with_registry(
404        client_id: ClientId,
405        venue: Option<Venue>,
406        registry: &SocketReconnectRegistry,
407    ) -> Self {
408        Self::from_registrar(client_id, venue, Some(registry.registrar()))
409    }
410
411    /// Creates a control for one logical socket endpoint.
412    #[must_use]
413    pub fn control(&self, endpoint: impl AsRef<str>) -> SocketControl {
414        let endpoint = Ustr::from(endpoint.as_ref());
415        let mut controls = self.controls.lock();
416        if let Some(control) = controls.get(&endpoint) {
417            return control.clone();
418        }
419
420        let control = SocketControl {
421            publisher: SocketStatePublisher {
422                client_id: self.client_id,
423                venue: self.venue,
424                endpoint,
425                sender: self.sender.clone(),
426                active_generation: Arc::new(AtomicU64::new(0)),
427                publish_lock: Arc::new(Mutex::new(())),
428            },
429            owner: self.owner.clone(),
430            generation: AtomicU64::new(0),
431            registration: Mutex::new(None),
432        };
433
434        controls.insert(endpoint, control.clone());
435        control
436    }
437
438    fn from_registrar(
439        client_id: ClientId,
440        venue: Option<Venue>,
441        registrar: Option<SocketReconnectRegistrar>,
442    ) -> Self {
443        Self {
444            client_id,
445            venue,
446            sender: try_get_system_event_sender(),
447            owner: registrar.and_then(|registrar| registrar.owner(client_id)),
448            controls: Arc::new(Mutex::new(AHashMap::new())),
449        }
450    }
451}
452
453#[derive(Clone, Debug)]
454struct SocketStatePublisher {
455    client_id: ClientId,
456    venue: Option<Venue>,
457    endpoint: Ustr,
458    sender: Option<EventSender<SystemEvent>>,
459    active_generation: Arc<AtomicU64>,
460    publish_lock: Arc<Mutex<()>>,
461}
462
463impl SocketStatePublisher {
464    fn publish(&self, state: SocketState) {
465        let Some(sender) = &self.sender else {
466            return;
467        };
468
469        let state = match state {
470            SocketState::Connected => SystemSocketState::Connected,
471            SocketState::Disconnected => SystemSocketState::Disconnected,
472        };
473
474        let change = SocketStateChange::new(self.client_id, self.venue, self.endpoint, state);
475        if let Err(e) = sender.send(SystemEvent::SocketState(change)) {
476            log::error!("Failed to emit socket state change: {e}");
477        }
478    }
479
480    fn is_current(&self, generation: u64) -> bool {
481        self.active_generation.load(Ordering::Acquire) == generation
482    }
483
484    fn publish_if_current<F>(&self, generation: u64, state: SocketState, on_state: &F)
485    where
486        F: Fn(SocketState),
487    {
488        let _guard = self.publish_lock.lock();
489
490        if self.is_current(generation) {
491            on_state(state);
492            self.publish(state);
493        }
494    }
495}
496
497/// State publisher and reconnect registration for one logical socket endpoint.
498///
499/// A clone starts without ownership of the source control's active transport generation.
500#[derive(Debug)]
501pub struct SocketControl {
502    publisher: SocketStatePublisher,
503    owner: Option<SocketReconnectOwner>,
504    generation: AtomicU64,
505    registration: Mutex<Option<SocketReconnectRegistration>>,
506}
507
508impl Clone for SocketControl {
509    fn clone(&self) -> Self {
510        Self {
511            publisher: self.publisher.clone(),
512            owner: self.owner.clone(),
513            generation: AtomicU64::new(0),
514            registration: Mutex::new(None),
515        }
516    }
517}
518
519impl SocketControl {
520    /// Creates a new [`SocketControl`] instance.
521    #[must_use]
522    pub fn new(client_id: ClientId, venue: Option<Venue>, endpoint: impl AsRef<str>) -> Self {
523        SocketControlFactory::new(client_id, venue).control(endpoint)
524    }
525
526    /// Creates a control which registers with an explicit live socket registry.
527    #[must_use]
528    pub fn with_registry(
529        client_id: ClientId,
530        venue: Option<Venue>,
531        endpoint: impl AsRef<str>,
532        registry: &SocketReconnectRegistry,
533    ) -> Self {
534        SocketControlFactory::with_registry(client_id, venue, registry).control(endpoint)
535    }
536
537    /// Returns a sink which publishes transport state changes as system events.
538    #[must_use]
539    pub fn sink(&self) -> SocketStateSink {
540        self.sink_with(|_| {})
541    }
542
543    /// Returns a state sink which calls `on_state` before publishing each change.
544    ///
545    /// `on_state` must not synchronously trigger another state change for the same endpoint.
546    #[must_use]
547    pub fn sink_with<F>(&self, on_state: F) -> SocketStateSink
548    where
549        F: Fn(SocketState) + Send + Sync + 'static,
550    {
551        let (registration, replaced, generation) = {
552            let _guard = self.publisher.publish_lock.lock();
553            let replaced = self
554                .owner
555                .as_ref()
556                .and_then(|owner| owner.remove(self.publisher.endpoint));
557            let generation = advance_generation(&self.publisher.active_generation);
558            self.generation.store(generation, Ordering::Release);
559            let registration = self.registration.lock().take();
560            (registration, replaced, generation)
561        };
562
563        deactivate(replaced);
564        drop(registration);
565
566        let publisher = self.publisher.clone();
567
568        SocketStateSink::new(move |state| {
569            publisher.publish_if_current(generation, state, &on_state);
570        })
571    }
572
573    /// Registers a reconnect request function for this endpoint's active generation.
574    pub fn register<F>(&self, request: F)
575    where
576        F: Fn() -> ReconnectRequestOutcome + Send + Sync + 'static,
577    {
578        let (replaced, old_registration) = {
579            let _guard = self.publisher.publish_lock.lock();
580            let generation = self.generation.load(Ordering::Acquire);
581            if generation == 0 || !self.publisher.is_current(generation) {
582                return;
583            }
584
585            let (registration, replaced) = if let Some(owner) = &self.owner {
586                owner.register(self.publisher.endpoint, SocketReconnectHandle::new(request))
587            } else {
588                (None, None)
589            };
590
591            let mut current = self.registration.lock();
592            let old_registration = std::mem::replace(&mut *current, registration);
593            (replaced, old_registration)
594        };
595
596        deactivate(replaced);
597        drop(old_registration);
598    }
599
600    /// Removes the reconnect handle owned by this control generation.
601    pub fn deregister(&self) {
602        let registration = {
603            let _guard = self.publisher.publish_lock.lock();
604            self.generation.store(0, Ordering::Release);
605            self.registration.lock().take()
606        };
607
608        drop(registration);
609    }
610}
611
612impl Drop for SocketControl {
613    fn drop(&mut self) {
614        self.deregister();
615    }
616}
617
618fn advance_generation(counter: &AtomicU64) -> u64 {
619    let mut current = counter.load(Ordering::Relaxed);
620
621    loop {
622        let next = current.wrapping_add(1).max(1);
623        match counter.compare_exchange_weak(current, next, Ordering::Release, Ordering::Relaxed) {
624            Ok(_) => return next,
625            Err(actual) => current = actual,
626        }
627    }
628}
629
630#[cfg(test)]
631mod tests {
632    use std::{
633        sync::{
634            Arc, Barrier,
635            atomic::{AtomicUsize, Ordering},
636        },
637        thread,
638        time::{Duration, Instant},
639    };
640
641    use rstest::rstest;
642
643    use super::*;
644
645    const ENDPOINT: &str = "test-streams";
646
647    fn control(registry: &SocketReconnectRegistry) -> SocketControl {
648        SocketControl::with_registry(
649            ClientId::from("TEST"),
650            Some(Venue::from("TEST")),
651            ENDPOINT,
652            registry,
653        )
654    }
655
656    fn handle(registry: &SocketReconnectRegistry) -> SocketReconnectHandle {
657        let SocketReconnectLookup::Handle(handle) =
658            registry.get(ClientId::from("TEST"), Ustr::from(ENDPOINT))
659        else {
660            panic!("test socket endpoint should be registered");
661        };
662
663        handle
664    }
665
666    #[rstest]
667    fn publishes_endpoint_state() {
668        let registry = SocketReconnectRegistry::default();
669        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
670        let mut control = control(&registry);
671        control.publisher.sender = Some(sender.into());
672        let _sink = control.sink();
673
674        control.publisher.publish(SocketState::Connected);
675        control.publisher.publish(SocketState::Disconnected);
676
677        let SystemEvent::SocketState(connected) = receiver.try_recv().unwrap();
678        let SystemEvent::SocketState(disconnected) = receiver.try_recv().unwrap();
679        assert_eq!(connected.client_id, ClientId::from("TEST"));
680        assert_eq!(connected.venue, Some(Venue::from("TEST")));
681        assert_eq!(connected.endpoint, Ustr::from(ENDPOINT));
682        assert_eq!(connected.state, SystemSocketState::Connected);
683        assert_eq!(disconnected.client_id, ClientId::from("TEST"));
684        assert_eq!(disconnected.venue, Some(Venue::from("TEST")));
685        assert_eq!(disconnected.endpoint, Ustr::from(ENDPOINT));
686        assert_eq!(disconnected.state, SystemSocketState::Disconnected);
687    }
688
689    #[rstest]
690    fn replacement_sink_suppresses_stale_transport_state() {
691        let registry = SocketReconnectRegistry::default();
692        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
693        let mut first = control(&registry);
694        first.publisher.sender = Some(sender.clone().into());
695        let mut replacement = first.clone();
696        replacement.publisher.sender = Some(sender.into());
697        let _stale_sink = first.sink();
698        let stale_generation = first.generation.load(Ordering::Acquire);
699        let _current_sink = replacement.sink();
700        let current_generation = replacement.generation.load(Ordering::Acquire);
701
702        first
703            .publisher
704            .publish_if_current(stale_generation, SocketState::Disconnected, &|_| {});
705        replacement.publisher.publish_if_current(
706            current_generation,
707            SocketState::Connected,
708            &|_| {},
709        );
710
711        let SystemEvent::SocketState(change) = receiver.try_recv().unwrap();
712        assert_eq!(change.state, SystemSocketState::Connected);
713        assert!(receiver.try_recv().is_err());
714    }
715
716    #[rstest]
717    #[case(ReconnectRequestOutcome::Accepted)]
718    #[case(ReconnectRequestOutcome::AlreadyReconnecting)]
719    #[case(ReconnectRequestOutcome::Disconnected)]
720    #[case(ReconnectRequestOutcome::Closed)]
721    #[case(ReconnectRequestOutcome::Unsupported)]
722    fn register_preserves_transport_outcome(#[case] expected: ReconnectRequestOutcome) {
723        let registry = SocketReconnectRegistry::default();
724        let control = control(&registry);
725        let _sink = control.sink();
726        control.register(move || expected);
727
728        assert_eq!(handle(&registry).request_reconnect(), expected);
729    }
730
731    #[rstest]
732    fn replacement_invalidates_stale_handle() {
733        let registry = SocketReconnectRegistry::default();
734        let factory = SocketControlFactory::with_registry(
735            ClientId::from("TEST"),
736            Some(Venue::from("TEST")),
737            &registry,
738        );
739        let first = factory.control(ENDPOINT);
740        let _first_sink = first.sink();
741        first.register(|| ReconnectRequestOutcome::Accepted);
742        let stale_handle = handle(&registry);
743        let replacement = factory.control(ENDPOINT);
744        let _replacement_sink = replacement.sink();
745        replacement.register(|| ReconnectRequestOutcome::AlreadyReconnecting);
746
747        first.register(|| ReconnectRequestOutcome::Disconnected);
748        first.deregister();
749
750        assert_eq!(
751            stale_handle.request_reconnect(),
752            ReconnectRequestOutcome::Closed
753        );
754        assert_eq!(
755            handle(&registry).request_reconnect(),
756            ReconnectRequestOutcome::AlreadyReconnecting
757        );
758    }
759
760    #[rstest]
761    fn replacement_waits_for_an_inflight_request_that_publishes_state() {
762        let registry = SocketReconnectRegistry::default();
763        let factory = SocketControlFactory::with_registry(
764            ClientId::from("TEST"),
765            Some(Venue::from("TEST")),
766            &registry,
767        );
768        let first = factory.control(ENDPOINT);
769        let _first_sink = first.sink();
770        let generation = first.generation.load(Ordering::Acquire);
771        let publisher = first.publisher.clone();
772        let entered = Arc::new(Barrier::new(2));
773        let release = Arc::new(Barrier::new(2));
774        let request_entered = Arc::clone(&entered);
775        let request_release = Arc::clone(&release);
776        first.register(move || {
777            request_entered.wait();
778            request_release.wait();
779            publisher.publish_if_current(generation, SocketState::Disconnected, &|_| {});
780            ReconnectRequestOutcome::Accepted
781        });
782
783        let stale_handle = handle(&registry);
784        let request_handle = stale_handle.clone();
785
786        let request = thread::spawn(move || request_handle.request_reconnect());
787        entered.wait();
788
789        let replacement = factory.control(ENDPOINT);
790        let (replaced_tx, replaced_rx) = std::sync::mpsc::channel();
791
792        let replace = thread::spawn(move || {
793            let _replacement_sink = replacement.sink();
794            replaced_tx.send(()).unwrap();
795        });
796
797        let deadline = Instant::now() + Duration::from_secs(1);
798
799        while !matches!(
800            registry.get(ClientId::from("TEST"), Ustr::from(ENDPOINT)),
801            SocketReconnectLookup::EndpointNotFound
802        ) {
803            assert!(
804                Instant::now() < deadline,
805                "replacement did not remove the stale entry"
806            );
807            thread::yield_now();
808        }
809
810        assert!(replaced_rx.try_recv().is_err());
811        release.wait();
812        assert_eq!(request.join().unwrap(), ReconnectRequestOutcome::Accepted);
813        replace.join().unwrap();
814        assert!(replaced_rx.try_recv().is_ok());
815        assert_eq!(
816            stale_handle.request_reconnect(),
817            ReconnectRequestOutcome::Closed
818        );
819    }
820
821    #[rstest]
822    fn reregistration_waits_for_an_inflight_request_that_publishes_state() {
823        let registry = SocketReconnectRegistry::default();
824        let control = Arc::new(control(&registry));
825        let _sink = control.sink();
826        let generation = control.generation.load(Ordering::Acquire);
827        let publisher = control.publisher.clone();
828        let entered = Arc::new(Barrier::new(2));
829        let release = Arc::new(Barrier::new(2));
830        let request_entered = Arc::clone(&entered);
831        let request_release = Arc::clone(&release);
832        control.register(move || {
833            request_entered.wait();
834            request_release.wait();
835            publisher.publish_if_current(generation, SocketState::Disconnected, &|_| {});
836            ReconnectRequestOutcome::Accepted
837        });
838
839        let stale_handle = handle(&registry);
840
841        let key = SocketEndpoint {
842            client_id: ClientId::from("TEST"),
843            endpoint: Ustr::from(ENDPOINT),
844        };
845
846        let old_generation = registry
847            .inner
848            .lock()
849            .entries
850            .get(&key)
851            .and_then(|entries| entries.values().next())
852            .map(|entry| entry.generation)
853            .unwrap();
854        let request_handle = stale_handle.clone();
855
856        let request = thread::spawn(move || request_handle.request_reconnect());
857        entered.wait();
858
859        let replacement = Arc::clone(&control);
860        let (registered_tx, registered_rx) = std::sync::mpsc::channel();
861
862        let register = thread::spawn(move || {
863            replacement.register(|| ReconnectRequestOutcome::AlreadyReconnecting);
864            registered_tx.send(()).unwrap();
865        });
866
867        let deadline = Instant::now() + Duration::from_secs(1);
868
869        loop {
870            let old_entry_is_registered = registry
871                .inner
872                .lock()
873                .entries
874                .get(&key)
875                .and_then(|entries| entries.values().next())
876                .is_some_and(|entry| entry.generation == old_generation);
877
878            if !old_entry_is_registered {
879                break;
880            }
881
882            assert!(
883                Instant::now() < deadline,
884                "replacement reconnect handle was not registered"
885            );
886            thread::yield_now();
887        }
888
889        assert!(registered_rx.try_recv().is_err());
890        release.wait();
891        assert_eq!(request.join().unwrap(), ReconnectRequestOutcome::Accepted);
892        register.join().unwrap();
893        assert!(registered_rx.try_recv().is_ok());
894        assert_eq!(
895            stale_handle.request_reconnect(),
896            ReconnectRequestOutcome::Closed
897        );
898        assert_eq!(
899            handle(&registry).request_reconnect(),
900            ReconnectRequestOutcome::AlreadyReconnecting
901        );
902    }
903
904    #[rstest]
905    fn registry_drop_revokes_a_retained_handle() {
906        let registry = SocketReconnectRegistry::default();
907        let control = control(&registry);
908        let _sink = control.sink();
909        control.register(|| ReconnectRequestOutcome::Accepted);
910        let retained = handle(&registry);
911
912        drop(registry);
913
914        assert_eq!(
915            retained.request_reconnect(),
916            ReconnectRequestOutcome::Closed
917        );
918    }
919
920    #[rstest]
921    fn deregister_prevents_delayed_registration() {
922        let registry = SocketReconnectRegistry::default();
923        let control = control(&registry);
924        let _sink = control.sink();
925
926        control.deregister();
927        control.register(|| ReconnectRequestOutcome::Accepted);
928
929        assert!(matches!(
930            registry.get(ClientId::from("TEST"), Ustr::from(ENDPOINT)),
931            SocketReconnectLookup::EndpointNotFound
932        ));
933    }
934
935    #[rstest]
936    fn ambiguous_endpoint_does_not_invoke_either_owner() {
937        let registry = SocketReconnectRegistry::default();
938        let first_count = Arc::new(AtomicUsize::new(0));
939        let second_count = Arc::new(AtomicUsize::new(0));
940        let first = control(&registry);
941        let second = control(&registry);
942        let _first_sink = first.sink();
943        let _second_sink = second.sink();
944        let first_callback = Arc::clone(&first_count);
945        first.register(move || {
946            first_callback.fetch_add(1, Ordering::SeqCst);
947            ReconnectRequestOutcome::Accepted
948        });
949
950        let second_callback = Arc::clone(&second_count);
951        second.register(move || {
952            second_callback.fetch_add(1, Ordering::SeqCst);
953            ReconnectRequestOutcome::Accepted
954        });
955
956        assert!(matches!(
957            registry.get(ClientId::from("TEST"), Ustr::from(ENDPOINT)),
958            SocketReconnectLookup::AmbiguousEndpoint
959        ));
960        assert_eq!(first_count.load(Ordering::SeqCst), 0);
961        assert_eq!(second_count.load(Ordering::SeqCst), 0);
962    }
963
964    #[rstest]
965    fn distinguishes_unknown_unsupported_and_missing_endpoint() {
966        let registry = SocketReconnectRegistry::default();
967        registry.register_client(ClientId::from("UNSUPPORTED"));
968        let _control = control(&registry);
969
970        assert!(matches!(
971            registry.get(ClientId::from("UNKNOWN"), Ustr::from(ENDPOINT)),
972            SocketReconnectLookup::ClientNotFound
973        ));
974        assert!(matches!(
975            registry.get(ClientId::from("UNSUPPORTED"), Ustr::from(ENDPOINT)),
976            SocketReconnectLookup::Unsupported
977        ));
978        assert!(matches!(
979            registry.get(ClientId::from("TEST"), Ustr::from("unknown")),
980            SocketReconnectLookup::EndpointNotFound
981        ));
982    }
983
984    #[rstest]
985    fn scope_restores_the_prior_registry() {
986        let outer = SocketReconnectRegistry::default();
987        let inner = SocketReconnectRegistry::default();
988
989        let (outer_control, inner_control) = outer.scope(|| {
990            let outer_control =
991                SocketControl::new(ClientId::from("OUTER"), Some(Venue::from("TEST")), ENDPOINT);
992
993            let inner_control = inner.scope(|| {
994                SocketControl::new(ClientId::from("INNER"), Some(Venue::from("TEST")), ENDPOINT)
995            });
996
997            let _sink = inner_control.sink();
998            inner_control.register(|| ReconnectRequestOutcome::Accepted);
999            (outer_control, inner_control)
1000        });
1001
1002        let _sink = outer_control.sink();
1003        outer_control.register(|| ReconnectRequestOutcome::Accepted);
1004
1005        assert!(matches!(
1006            outer.get(ClientId::from("OUTER"), Ustr::from(ENDPOINT)),
1007            SocketReconnectLookup::Handle(_)
1008        ));
1009        assert!(matches!(
1010            inner.get(ClientId::from("INNER"), Ustr::from(ENDPOINT)),
1011            SocketReconnectLookup::Handle(_)
1012        ));
1013        drop(inner_control);
1014    }
1015}