Skip to main content

whatsapp_rust/client/
extension_lifecycle.rs

1use std::cell::Cell;
2use std::collections::VecDeque;
3use std::fmt;
4use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
5use std::sync::{Arc, Weak};
6use std::time::Duration;
7
8use super::Client;
9use futures::FutureExt;
10use wacore::runtime::{BoxFuture, Runtime, ShutdownNotifier, ShutdownSignal, wait_for_shutdown};
11
12const SCOPE_OPEN: u8 = 0;
13const SCOPE_READY: u8 = 1;
14const SCOPE_CANCELLED: u8 = 2;
15const SCOPE_CLOSED: u8 = 3;
16const CONSTRUCTION_INSTALLING: u8 = 0;
17const CONSTRUCTION_ACTIVE: u8 = 1;
18const CONSTRUCTION_REJECTED: u8 = 2;
19const CALLBACK_TIMEOUT: Duration = Duration::from_secs(5);
20const CALLBACK_QUEUE_TARGET_CAPACITY: usize = 64;
21
22std::thread_local! {
23    static ACTIVE_CALLBACK: Cell<*const LifecycleRegistration> = const { Cell::new(std::ptr::null()) };
24    static ACTIVE_READY_PUBLICATION: Cell<*const LifecycleRegistration> = const { Cell::new(std::ptr::null()) };
25}
26
27/// Observable state of one authenticated connection generation.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum ConnectionScopeState {
31    Open,
32    Ready,
33    Cancelled,
34    Closed,
35}
36
37struct ConnectionScopeInner {
38    generation: u64,
39    state: AtomicU8,
40    cancellation: ShutdownNotifier,
41}
42
43/// Stable handle for work owned by one authenticated connection generation.
44///
45/// A scope is cancelled synchronously when its generation is retired and is
46/// marked closed only after the client's authoritative connection cleanup.
47#[derive(Clone)]
48pub struct ConnectionScope {
49    inner: Arc<ConnectionScopeInner>,
50}
51
52impl ConnectionScope {
53    pub(crate) fn new(generation: u64) -> Self {
54        Self {
55            inner: Arc::new(ConnectionScopeInner {
56                generation,
57                state: AtomicU8::new(SCOPE_OPEN),
58                cancellation: ShutdownNotifier::new(),
59            }),
60        }
61    }
62
63    pub fn generation(&self) -> u64 {
64        self.inner.generation
65    }
66
67    pub fn state(&self) -> ConnectionScopeState {
68        match self.inner.state.load(Ordering::Acquire) {
69            SCOPE_OPEN => ConnectionScopeState::Open,
70            SCOPE_READY => ConnectionScopeState::Ready,
71            SCOPE_CANCELLED => ConnectionScopeState::Cancelled,
72            _ => ConnectionScopeState::Closed,
73        }
74    }
75
76    /// Fires when this scope stops owning connection work, whether it is
77    /// cancelled during retirement or reaches final closure after cleanup.
78    pub fn cancellation_signal(&self) -> ShutdownSignal {
79        self.inner.cancellation.subscribe()
80    }
81
82    /// Returns `true` after either cancellation or final closure.
83    pub fn is_cancelled(&self) -> bool {
84        self.inner.state.load(Ordering::Acquire) >= SCOPE_CANCELLED
85    }
86
87    fn mark_ready(&self) -> bool {
88        self.inner
89            .state
90            .compare_exchange(SCOPE_OPEN, SCOPE_READY, Ordering::AcqRel, Ordering::Acquire)
91            .is_ok()
92    }
93
94    pub(crate) fn cancel(&self) {
95        let mut state = self.inner.state.load(Ordering::Acquire);
96        while state < SCOPE_CANCELLED {
97            match self.inner.state.compare_exchange_weak(
98                state,
99                SCOPE_CANCELLED,
100                Ordering::AcqRel,
101                Ordering::Acquire,
102            ) {
103                Ok(_) => {
104                    self.inner.cancellation.notify();
105                    return;
106                }
107                Err(actual) => state = actual,
108            }
109        }
110    }
111
112    fn close(&self) {
113        let previous = self.inner.state.swap(SCOPE_CLOSED, Ordering::AcqRel);
114        if previous < SCOPE_CANCELLED {
115            self.inner.cancellation.notify();
116        }
117    }
118}
119
120impl fmt::Debug for ConnectionScope {
121    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
122        formatter
123            .debug_struct("ConnectionScope")
124            .field("generation", &self.generation())
125            .field("state", &self.state())
126            .finish()
127    }
128}
129
130/// Aggregate lifecycle seam installed during [`Client`](super::Client) construction.
131///
132/// Implementations must make `install` transactional. Connection callbacks are
133/// serialized, with bounded ready work; connection cleanup only schedules
134/// `on_closed` so a stalled extension cannot block reconnect. Closure callbacks
135/// are lossless and may temporarily exceed the target capacity. A future plugin
136/// host owns per-plugin ordering and isolation behind this client-level seam.
137/// `install` receives a weak client reference so retaining it cannot create a cycle.
138/// `signal_shutdown` is the non-blocking boundary for resources that must stop
139/// even when an FFI host cannot await `shutdown`.
140pub trait ClientLifecycle: wacore::sync_marker::MaybeSendSync {
141    fn install<'a>(&'a self, _client: Weak<Client>) -> BoxFuture<'a, anyhow::Result<()>> {
142        Box::pin(async { Ok(()) })
143    }
144
145    fn on_ready<'a>(&'a self, _scope: ConnectionScope) -> BoxFuture<'a, anyhow::Result<()>> {
146        Box::pin(async { Ok(()) })
147    }
148
149    fn on_closed<'a>(&'a self, _scope: ConnectionScope) -> BoxFuture<'a, anyhow::Result<()>> {
150        Box::pin(async { Ok(()) })
151    }
152
153    /// Stop synchronously owned resources before asynchronous shutdown begins.
154    /// Implementations must return promptly and make repeated calls harmless.
155    fn signal_shutdown(&self) {}
156
157    fn shutdown(&self) -> BoxFuture<'_, anyhow::Result<()>> {
158        Box::pin(async { Ok(()) })
159    }
160}
161
162pub(super) struct LifecycleRegistration {
163    handler: Arc<dyn ClientLifecycle>,
164    runtime: Arc<dyn Runtime>,
165    ready_publication: std::sync::Mutex<()>,
166    scopes: std::sync::Mutex<ScopeRegistry>,
167    callback_queue: std::sync::Mutex<CallbackQueue>,
168    shutdown_complete: AtomicBool,
169    shutdown_notifier: ShutdownNotifier,
170    callback_timeout: Duration,
171    terminal: AtomicBool,
172    construction_transition: std::sync::Mutex<()>,
173    construction_state: AtomicU8,
174    construction_notifier: ShutdownNotifier,
175}
176
177#[derive(Default)]
178struct ScopeRegistry {
179    active: Option<ConnectionScope>,
180    retired: Vec<ConnectionScope>,
181}
182
183enum LifecycleCallback {
184    Ready {
185        scope: ConnectionScope,
186        done: async_channel::Sender<bool>,
187    },
188    Closed(ConnectionScope),
189    Shutdown,
190}
191
192#[derive(Default)]
193struct CallbackQueue {
194    pending: VecDeque<LifecycleCallback>,
195    shutdown_requested: bool,
196    shutdown_enqueued: bool,
197    drain_scheduled: bool,
198    overflowed: bool,
199}
200
201impl CallbackQueue {
202    fn push_with_pressure_policy(&mut self, callback: LifecycleCallback) -> Vec<LifecycleCallback> {
203        if self.pending.len() < CALLBACK_QUEUE_TARGET_CAPACITY && !self.overflowed {
204            self.pending.push_back(callback);
205            return Vec::new();
206        }
207
208        self.overflowed |= self.pending.len() >= CALLBACK_QUEUE_TARGET_CAPACITY;
209        match callback {
210            callback @ LifecycleCallback::Ready { .. } => {
211                let mut dropped = Vec::new();
212                let mut retained = VecDeque::with_capacity(self.pending.len());
213                for pending in self.pending.drain(..) {
214                    if matches!(pending, LifecycleCallback::Ready { .. }) {
215                        dropped.push(pending);
216                    } else {
217                        retained.push_back(pending);
218                    }
219                }
220                self.pending = retained;
221                self.pending.push_back(callback);
222                dropped
223            }
224            callback => {
225                // Closures are lossless, so the target remains soft under backlog.
226                self.pending.push_back(callback);
227                Vec::new()
228            }
229        }
230    }
231
232    fn compact_for_shutdown(&mut self) -> Vec<LifecycleCallback> {
233        if !self.overflowed {
234            return Vec::new();
235        }
236
237        let mut retained = VecDeque::with_capacity(self.pending.len());
238        let mut dropped = Vec::new();
239        for callback in self.pending.drain(..) {
240            match callback {
241                callback @ LifecycleCallback::Closed(_) => retained.push_back(callback),
242                callback => dropped.push(callback),
243            }
244        }
245        self.pending = retained;
246        dropped
247    }
248}
249
250struct CallbackContextGuard {
251    previous: *const LifecycleRegistration,
252}
253
254struct ReadyPublicationGuard {
255    previous: *const LifecycleRegistration,
256}
257
258impl ReadyPublicationGuard {
259    fn enter(registration: &LifecycleRegistration) -> Self {
260        let previous = ACTIVE_READY_PUBLICATION.replace(registration);
261        Self { previous }
262    }
263}
264
265impl Drop for ReadyPublicationGuard {
266    fn drop(&mut self) {
267        ACTIVE_READY_PUBLICATION.set(self.previous);
268    }
269}
270
271impl CallbackContextGuard {
272    fn enter(registration: &LifecycleRegistration) -> Self {
273        let previous = ACTIVE_CALLBACK.replace(registration);
274        Self { previous }
275    }
276}
277
278impl Drop for CallbackContextGuard {
279    fn drop(&mut self) {
280        ACTIVE_CALLBACK.set(self.previous);
281    }
282}
283
284fn callback_context_active(registration: &LifecycleRegistration) -> bool {
285    ACTIVE_CALLBACK.with(|active| std::ptr::eq(active.get(), registration))
286}
287
288fn ready_publication_active(registration: &LifecycleRegistration) -> bool {
289    ACTIVE_READY_PUBLICATION.with(|active| std::ptr::eq(active.get(), registration))
290}
291
292impl LifecycleRegistration {
293    pub(super) fn new(handler: Arc<dyn ClientLifecycle>, runtime: Arc<dyn Runtime>) -> Self {
294        Self::new_with_timeout(handler, runtime, CALLBACK_TIMEOUT)
295    }
296
297    pub(super) fn new_with_timeout(
298        handler: Arc<dyn ClientLifecycle>,
299        runtime: Arc<dyn Runtime>,
300        callback_timeout: Duration,
301    ) -> Self {
302        Self {
303            handler,
304            runtime,
305            ready_publication: std::sync::Mutex::new(()),
306            scopes: std::sync::Mutex::new(ScopeRegistry::default()),
307            callback_queue: std::sync::Mutex::new(CallbackQueue::default()),
308            shutdown_complete: AtomicBool::new(false),
309            shutdown_notifier: ShutdownNotifier::new(),
310            callback_timeout,
311            terminal: AtomicBool::new(false),
312            construction_transition: std::sync::Mutex::new(()),
313            construction_state: AtomicU8::new(CONSTRUCTION_INSTALLING),
314            construction_notifier: ShutdownNotifier::new(),
315        }
316    }
317
318    pub(super) async fn install(&self, client: Weak<Client>) -> anyhow::Result<()> {
319        let rejected = self.construction_notifier.subscribe();
320        if self.construction_state.load(Ordering::Acquire) == CONSTRUCTION_REJECTED {
321            return Err(anyhow::anyhow!(
322                "client shutdown began during lifecycle installation"
323            ));
324        }
325        let mut install = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
326            self.handler.install(client)
327        }))
328        .map_err(|_| anyhow::anyhow!("lifecycle install panicked before returning a future"))?;
329        let cancelled = Box::pin(wait_for_shutdown(&rejected));
330        let result = {
331            let install_poll = std::future::poll_fn(|context| install.as_mut().poll(context));
332            let install_poll = Box::pin(std::panic::AssertUnwindSafe(install_poll).catch_unwind());
333            match futures::future::select(cancelled, install_poll).await {
334                futures::future::Either::Left((_, install_poll)) => {
335                    drop(install_poll);
336                    None
337                }
338                futures::future::Either::Right((result, _)) => Some(result),
339            }
340        };
341        let drop_panicked =
342            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(install))).is_err();
343        if drop_panicked {
344            return Err(anyhow::anyhow!(
345                "lifecycle install future panicked while being dropped"
346            ));
347        }
348        let Some(result) = result else {
349            return Err(anyhow::anyhow!(
350                "client shutdown began during lifecycle installation"
351            ));
352        };
353        let result = result.map_err(|_| anyhow::anyhow!("lifecycle install future panicked"))?;
354        if result.is_ok()
355            && self.construction_state.load(Ordering::Acquire) == CONSTRUCTION_REJECTED
356        {
357            return Err(anyhow::anyhow!(
358                "client shutdown began during lifecycle installation"
359            ));
360        }
361        result
362    }
363
364    pub(super) fn activate(&self) -> bool {
365        self.activate_with(|| true)
366    }
367
368    pub(super) fn activate_with(&self, commit: impl FnOnce() -> bool) -> bool {
369        let _transition = self
370            .construction_transition
371            .lock()
372            .unwrap_or_else(|poisoned| poisoned.into_inner());
373        if self.terminal.load(Ordering::Acquire) {
374            self.reject_construction();
375            return false;
376        }
377        if self.construction_state.load(Ordering::Acquire) == CONSTRUCTION_ACTIVE {
378            return true;
379        }
380        if !commit() {
381            self.reject_construction();
382            return false;
383        }
384        match self.construction_state.compare_exchange(
385            CONSTRUCTION_INSTALLING,
386            CONSTRUCTION_ACTIVE,
387            Ordering::AcqRel,
388            Ordering::Acquire,
389        ) {
390            Ok(_) => self.construction_notifier.notify(),
391            Err(CONSTRUCTION_ACTIVE) => {}
392            Err(_) => return false,
393        }
394        true
395    }
396
397    pub(super) async fn wait_until_active(&self) -> bool {
398        let activated = self.construction_notifier.subscribe();
399        match self.construction_state.load(Ordering::Acquire) {
400            CONSTRUCTION_ACTIVE => return !self.terminal.load(Ordering::Acquire),
401            CONSTRUCTION_REJECTED => return false,
402            _ => {}
403        }
404        wait_for_shutdown(&activated).await;
405        self.construction_state.load(Ordering::Acquire) == CONSTRUCTION_ACTIVE
406            && !self.terminal.load(Ordering::Acquire)
407    }
408
409    pub(super) fn begin_scope_if_current(
410        &self,
411        generation: u64,
412        is_current: impl FnOnce() -> bool,
413    ) -> bool {
414        if self.terminal.load(Ordering::Acquire) {
415            return false;
416        }
417
418        let scope = ConnectionScope::new(generation);
419        let mut scopes = self.scopes();
420        if self.terminal.load(Ordering::Acquire) || !is_current() {
421            return false;
422        }
423        let replaced = scopes.active.replace(scope);
424        if let Some(replaced) = replaced {
425            log::warn!(
426                "Replacing unclosed connection scope for generation {}",
427                replaced.generation()
428            );
429            replaced.cancel();
430            scopes.retired.push(replaced);
431        }
432        true
433    }
434
435    pub(super) async fn ready(self: &Arc<Self>, generation: u64) -> bool {
436        if self.terminal.load(Ordering::Acquire) {
437            return false;
438        }
439        let (done_tx, done_rx) = async_channel::bounded(1);
440        let scope = {
441            let scopes = self.scopes();
442            let scope = scopes
443                .active
444                .as_ref()
445                .filter(|scope| scope.generation() == generation)
446                .or_else(|| {
447                    scopes
448                        .retired
449                        .iter()
450                        .find(|scope| scope.generation() == generation)
451                })
452                .cloned();
453            let Some(scope) = scope.filter(ConnectionScope::mark_ready) else {
454                return false;
455            };
456            self.enqueue_callback(LifecycleCallback::Ready {
457                scope: scope.clone(),
458                done: done_tx,
459            });
460            scope
461        };
462
463        done_rx.recv().await.unwrap_or(false) && !scope.is_cancelled()
464    }
465
466    pub(super) fn publish_ready(&self, generation: u64, publish: impl FnOnce()) -> bool {
467        let _publication = self.ready_publication();
468        if self.terminal.load(Ordering::Acquire) {
469            return false;
470        }
471        let Some(scope) = self.scope_for(generation) else {
472            return false;
473        };
474        if scope.state() != ConnectionScopeState::Ready {
475            return false;
476        }
477
478        let _publication_context = ReadyPublicationGuard::enter(self);
479        publish();
480        true
481    }
482
483    pub(super) fn cancel_scope(&self, generation: u64) {
484        if ready_publication_active(self) {
485            self.cancel_scope_inner(generation);
486        } else {
487            let _publication = self.ready_publication();
488            self.cancel_scope_inner(generation);
489        }
490    }
491
492    pub(super) fn cancel_active_scope(&self) {
493        if ready_publication_active(self) {
494            self.cancel_active_scope_inner();
495        } else {
496            let _publication = self.ready_publication();
497            self.cancel_active_scope_inner();
498        }
499    }
500
501    pub(super) fn close_scope(self: &Arc<Self>, generation: u64) {
502        self.close_scope_with(generation, || {});
503    }
504
505    /// `after_remove` runs with `scopes` held and before `callback_queue` is acquired.
506    /// It must not block an async executor or re-enter lifecycle APIs; blocking test hooks run
507    /// on a dedicated thread.
508    fn close_scope_with(self: &Arc<Self>, generation: u64, after_remove: impl FnOnce()) {
509        let (should_spawn, dropped) = {
510            let mut scopes = self.scopes();
511            let scope = if scopes
512                .active
513                .as_ref()
514                .is_some_and(|scope| scope.generation() == generation)
515            {
516                scopes.active.take()
517            } else {
518                scopes
519                    .retired
520                    .iter()
521                    .position(|scope| scope.generation() == generation)
522                    .map(|position| scopes.retired.remove(position))
523            };
524            let Some(scope) = scope else {
525                return;
526            };
527
528            scope.close();
529            after_remove();
530
531            // Publish closure before exposing an empty registry so terminal
532            // shutdown cannot overtake the final on_closed callback.
533            let no_open_scopes = scopes.active.is_none() && scopes.retired.is_empty();
534            let mut queue = self.callback_queue();
535            let mut dropped = Vec::new();
536            if queue.shutdown_requested {
537                dropped.extend(queue.push_with_pressure_policy(LifecycleCallback::Closed(scope)));
538                dropped.extend(queue.compact_for_shutdown());
539                if no_open_scopes && !queue.shutdown_enqueued {
540                    queue.shutdown_enqueued = true;
541                    queue.pending.push_back(LifecycleCallback::Shutdown);
542                }
543            } else {
544                dropped.extend(queue.push_with_pressure_policy(LifecycleCallback::Closed(scope)));
545            }
546            let should_spawn = !queue.pending.is_empty() && !queue.drain_scheduled;
547            queue.drain_scheduled |= should_spawn;
548            (should_spawn, dropped)
549        };
550
551        warn_dropped_callbacks(dropped);
552        self.spawn_callback_driver(should_spawn);
553    }
554
555    pub(super) async fn shutdown(self: &Arc<Self>) {
556        self.request_shutdown();
557
558        if self.shutdown_complete.load(Ordering::Acquire) || callback_context_active(self) {
559            return;
560        }
561
562        let completed = self.shutdown_notifier.subscribe();
563        if self.shutdown_complete.load(Ordering::Acquire) {
564            return;
565        }
566        wait_for_shutdown(&completed).await;
567    }
568
569    pub(super) fn request_shutdown(self: &Arc<Self>) {
570        self.signal_shutdown_sync();
571        {
572            let mut queue = self.callback_queue();
573            queue.shutdown_requested = true;
574        }
575        self.enqueue_shutdown_if_ready();
576    }
577
578    pub(super) fn signal_shutdown_sync(&self) {
579        let first_signal = {
580            let _transition = self
581                .construction_transition
582                .lock()
583                .unwrap_or_else(|poisoned| poisoned.into_inner());
584            self.reject_construction();
585            !self.terminal.swap(true, Ordering::AcqRel)
586        };
587        if ready_publication_active(self) {
588            self.cancel_all_scopes()
589        } else {
590            let _publication = self.ready_publication();
591            self.cancel_all_scopes()
592        }
593        if first_signal
594            && std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
595                self.handler.signal_shutdown();
596            }))
597            .is_err()
598        {
599            log::warn!("Client lifecycle synchronous shutdown signal panicked");
600        }
601    }
602
603    fn enqueue_callback(self: &Arc<Self>, callback: LifecycleCallback) {
604        let (should_spawn, dropped) = {
605            let mut queue = self.callback_queue();
606            if queue.shutdown_requested || self.terminal.load(Ordering::Acquire) {
607                (false, vec![callback])
608            } else {
609                let dropped = queue.push_with_pressure_policy(callback);
610                let should_spawn = !queue.drain_scheduled;
611                queue.drain_scheduled = true;
612                (should_spawn, dropped)
613            }
614        };
615        warn_dropped_callbacks(dropped);
616        self.spawn_callback_driver(should_spawn);
617    }
618
619    fn enqueue_shutdown_if_ready(self: &Arc<Self>) {
620        let no_open_scopes = {
621            let scopes = self.scopes();
622            scopes.active.is_none() && scopes.retired.is_empty()
623        };
624        let (should_spawn, dropped) = {
625            let mut queue = self.callback_queue();
626            if !queue.shutdown_requested || queue.shutdown_enqueued {
627                return;
628            }
629            let dropped = queue.compact_for_shutdown();
630            if no_open_scopes {
631                queue.shutdown_enqueued = true;
632                queue.pending.push_back(LifecycleCallback::Shutdown);
633            }
634            let should_spawn = !queue.pending.is_empty() && !queue.drain_scheduled;
635            queue.drain_scheduled |= should_spawn;
636            (should_spawn, dropped)
637        };
638        warn_dropped_callbacks(dropped);
639        self.spawn_callback_driver(should_spawn);
640    }
641
642    fn spawn_callback_driver(self: &Arc<Self>, should_spawn: bool) {
643        if !should_spawn {
644            return;
645        }
646        let registration = Arc::clone(self);
647        self.runtime
648            .spawn(Box::pin(async move {
649                registration.drive_callbacks().await;
650            }))
651            .detach();
652    }
653
654    async fn drive_callbacks(self: Arc<Self>) {
655        loop {
656            let callback = {
657                let mut queue = self.callback_queue();
658                match queue.pending.pop_front() {
659                    Some(callback) => callback,
660                    None => {
661                        queue.drain_scheduled = false;
662                        queue.overflowed = false;
663                        return;
664                    }
665                }
666            };
667
668            match callback {
669                LifecycleCallback::Ready { scope, done } => {
670                    let callback_scope = scope.clone();
671                    self.run_callback("on_ready", move |handler| handler.on_ready(callback_scope))
672                        .await;
673                    let _ = done.try_send(!scope.is_cancelled());
674                }
675                LifecycleCallback::Closed(scope) => {
676                    self.run_callback("on_closed", move |handler| handler.on_closed(scope))
677                        .await;
678                }
679                LifecycleCallback::Shutdown => {
680                    self.run_callback("shutdown", |handler| handler.shutdown())
681                        .await;
682                    self.shutdown_complete.store(true, Ordering::Release);
683                    self.shutdown_notifier.notify();
684                }
685            }
686        }
687    }
688
689    async fn run_callback<'a>(
690        &'a self,
691        name: &'static str,
692        create: impl FnOnce(&'a dyn ClientLifecycle) -> BoxFuture<'a, anyhow::Result<()>>,
693    ) {
694        let callback = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
695            let _callback_context = CallbackContextGuard::enter(self);
696            create(&*self.handler)
697        }));
698        let Ok(mut callback) = callback else {
699            log::warn!("Client lifecycle {name} panicked");
700            return;
701        };
702        let result = {
703            let callback_poll = std::future::poll_fn(|context| {
704                let _callback_context = CallbackContextGuard::enter(self);
705                callback.as_mut().poll(context)
706            });
707            let callback_poll =
708                Box::pin(std::panic::AssertUnwindSafe(callback_poll).catch_unwind());
709            match futures::future::select(callback_poll, self.runtime.sleep(self.callback_timeout))
710                .await
711            {
712                futures::future::Either::Left((result, _)) => Some(result),
713                futures::future::Either::Right(((), callback_poll)) => {
714                    drop(callback_poll);
715                    None
716                }
717            }
718        };
719        let drop_panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
720            let _callback_context = CallbackContextGuard::enter(self);
721            drop(callback);
722        }))
723        .is_err();
724        if drop_panicked {
725            log::warn!("Client lifecycle {name} panicked while being dropped");
726            return;
727        }
728        match result {
729            Some(Ok(Ok(()))) => {}
730            Some(Ok(Err(error))) => log::warn!("Client lifecycle {name} failed: {error:#}"),
731            Some(Err(_)) => log::warn!("Client lifecycle {name} panicked"),
732            None => log::warn!("Client lifecycle {name} timed out"),
733        }
734    }
735
736    fn scopes(&self) -> std::sync::MutexGuard<'_, ScopeRegistry> {
737        self.scopes
738            .lock()
739            .unwrap_or_else(|poisoned| poisoned.into_inner())
740    }
741
742    fn callback_queue(&self) -> std::sync::MutexGuard<'_, CallbackQueue> {
743        self.callback_queue
744            .lock()
745            .unwrap_or_else(|poisoned| poisoned.into_inner())
746    }
747
748    fn ready_publication(&self) -> std::sync::MutexGuard<'_, ()> {
749        self.ready_publication
750            .lock()
751            .unwrap_or_else(|poisoned| poisoned.into_inner())
752    }
753
754    fn cancel_scope_inner(&self, generation: u64) {
755        if let Some(scope) = self.scope_for(generation) {
756            scope.cancel();
757        }
758    }
759
760    fn reject_construction(&self) {
761        if self
762            .construction_state
763            .compare_exchange(
764                CONSTRUCTION_INSTALLING,
765                CONSTRUCTION_REJECTED,
766                Ordering::AcqRel,
767                Ordering::Acquire,
768            )
769            .is_ok()
770        {
771            self.construction_notifier.notify();
772        }
773    }
774
775    fn cancel_active_scope_inner(&self) {
776        if let Some(scope) = &self.scopes().active {
777            scope.cancel();
778        }
779    }
780
781    fn cancel_all_scopes(&self) {
782        let scopes = self.scopes();
783        if let Some(scope) = &scopes.active {
784            scope.cancel();
785        }
786        for scope in &scopes.retired {
787            scope.cancel();
788        }
789    }
790
791    fn scope_for(&self, generation: u64) -> Option<ConnectionScope> {
792        let scopes = self.scopes();
793        scopes
794            .active
795            .as_ref()
796            .filter(|scope| scope.generation() == generation)
797            .or_else(|| {
798                scopes
799                    .retired
800                    .iter()
801                    .find(|scope| scope.generation() == generation)
802            })
803            .cloned()
804    }
805}
806
807fn warn_dropped_callbacks(dropped: Vec<LifecycleCallback>) {
808    if !dropped.is_empty() {
809        log::warn!(
810            "Dropped {} stale client lifecycle callback(s) under queue pressure or terminal shutdown",
811            dropped.len()
812        );
813    }
814}
815
816#[cfg(test)]
817mod tests {
818    use std::sync::atomic::AtomicUsize;
819
820    use async_trait::async_trait;
821    use bytes::Bytes;
822
823    use super::*;
824    use crate::runtime_impl::TokioRuntime;
825    use crate::store::persistence_manager::PersistenceManager;
826    use crate::test_utils::MockHttpClient;
827    use crate::transport::mock::MockTransportFactory;
828
829    #[derive(Default)]
830    struct RecordingLifecycle {
831        events: std::sync::Mutex<Vec<String>>,
832        scopes: std::sync::Mutex<Vec<ConnectionScope>>,
833        shutdowns: AtomicUsize,
834    }
835
836    impl RecordingLifecycle {
837        fn events(&self) -> Vec<String> {
838            self.events
839                .lock()
840                .unwrap_or_else(|poisoned| poisoned.into_inner())
841                .clone()
842        }
843    }
844
845    impl ClientLifecycle for RecordingLifecycle {
846        fn install<'a>(&'a self, client: Weak<Client>) -> BoxFuture<'a, anyhow::Result<()>> {
847            Box::pin(async move {
848                assert!(client.upgrade().is_some());
849                self.events
850                    .lock()
851                    .unwrap_or_else(|poisoned| poisoned.into_inner())
852                    .push("install".to_string());
853                Ok(())
854            })
855        }
856
857        fn on_ready<'a>(&'a self, scope: ConnectionScope) -> BoxFuture<'a, anyhow::Result<()>> {
858            Box::pin(async move {
859                self.events
860                    .lock()
861                    .unwrap_or_else(|poisoned| poisoned.into_inner())
862                    .push(format!("ready:{}", scope.generation()));
863                self.scopes
864                    .lock()
865                    .unwrap_or_else(|poisoned| poisoned.into_inner())
866                    .push(scope);
867                Ok(())
868            })
869        }
870
871        fn on_closed<'a>(&'a self, scope: ConnectionScope) -> BoxFuture<'a, anyhow::Result<()>> {
872            Box::pin(async move {
873                self.events
874                    .lock()
875                    .unwrap_or_else(|poisoned| poisoned.into_inner())
876                    .push(format!("closed:{}", scope.generation()));
877                Ok(())
878            })
879        }
880
881        fn shutdown(&self) -> BoxFuture<'_, anyhow::Result<()>> {
882            Box::pin(async move {
883                self.shutdowns.fetch_add(1, Ordering::SeqCst);
884                self.events
885                    .lock()
886                    .unwrap_or_else(|poisoned| poisoned.into_inner())
887                    .push("shutdown".to_string());
888                Ok(())
889            })
890        }
891    }
892
893    struct BlockingDisconnect {
894        started: async_channel::Sender<()>,
895        release: async_channel::Receiver<()>,
896    }
897
898    #[async_trait]
899    impl crate::transport::Transport for BlockingDisconnect {
900        async fn send(&self, _data: Bytes) -> anyhow::Result<()> {
901            Ok(())
902        }
903
904        async fn disconnect(&self) {
905            let _ = self.started.try_send(());
906            let _ = self.release.recv().await;
907        }
908    }
909
910    struct PanickingDisconnect;
911
912    #[async_trait]
913    impl crate::transport::Transport for PanickingDisconnect {
914        async fn send(&self, _data: Bytes) -> anyhow::Result<()> {
915            Ok(())
916        }
917
918        async fn disconnect(&self) {
919            panic!("injected disconnect panic");
920        }
921    }
922
923    struct BlockingReadyLifecycle {
924        ready_started: async_channel::Sender<()>,
925        release_ready: async_channel::Receiver<()>,
926        scope: std::sync::Mutex<Option<ConnectionScope>>,
927        events: std::sync::Mutex<Vec<&'static str>>,
928    }
929
930    #[derive(Default)]
931    struct ReentrantDisconnectLifecycle {
932        client: std::sync::Mutex<Option<Weak<Client>>>,
933        events: std::sync::Mutex<Vec<&'static str>>,
934    }
935
936    struct ReentrantReconnectLifecycle {
937        client: std::sync::Mutex<Option<Weak<Client>>>,
938        events: std::sync::Mutex<Vec<&'static str>>,
939        immediate: bool,
940    }
941
942    impl ClientLifecycle for ReentrantReconnectLifecycle {
943        fn install<'a>(&'a self, client: Weak<Client>) -> BoxFuture<'a, anyhow::Result<()>> {
944            Box::pin(async move {
945                *self
946                    .client
947                    .lock()
948                    .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(client);
949                Ok(())
950            })
951        }
952
953        fn on_ready<'a>(&'a self, _scope: ConnectionScope) -> BoxFuture<'a, anyhow::Result<()>> {
954            Box::pin(async move {
955                self.events
956                    .lock()
957                    .unwrap_or_else(|poisoned| poisoned.into_inner())
958                    .push("ready-started");
959                let client = self
960                    .client
961                    .lock()
962                    .unwrap_or_else(|poisoned| poisoned.into_inner())
963                    .as_ref()
964                    .and_then(Weak::upgrade)
965                    .expect("installed client");
966                if self.immediate {
967                    client.reconnect_immediately().await;
968                } else {
969                    client.reconnect().await;
970                }
971                self.events
972                    .lock()
973                    .unwrap_or_else(|poisoned| poisoned.into_inner())
974                    .push("ready-finished");
975                Ok(())
976            })
977        }
978
979        fn on_closed<'a>(&'a self, _scope: ConnectionScope) -> BoxFuture<'a, anyhow::Result<()>> {
980            Box::pin(async move {
981                self.events
982                    .lock()
983                    .unwrap_or_else(|poisoned| poisoned.into_inner())
984                    .push("closed");
985                Ok(())
986            })
987        }
988
989        fn shutdown(&self) -> BoxFuture<'_, anyhow::Result<()>> {
990            Box::pin(async move {
991                self.events
992                    .lock()
993                    .unwrap_or_else(|poisoned| poisoned.into_inner())
994                    .push("shutdown");
995                Ok(())
996            })
997        }
998    }
999
1000    impl ClientLifecycle for ReentrantDisconnectLifecycle {
1001        fn install<'a>(&'a self, client: Weak<Client>) -> BoxFuture<'a, anyhow::Result<()>> {
1002            Box::pin(async move {
1003                *self
1004                    .client
1005                    .lock()
1006                    .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(client);
1007                Ok(())
1008            })
1009        }
1010
1011        fn on_ready<'a>(&'a self, _scope: ConnectionScope) -> BoxFuture<'a, anyhow::Result<()>> {
1012            Box::pin(async move {
1013                self.events
1014                    .lock()
1015                    .unwrap_or_else(|poisoned| poisoned.into_inner())
1016                    .push("ready-started");
1017                let client = self
1018                    .client
1019                    .lock()
1020                    .unwrap_or_else(|poisoned| poisoned.into_inner())
1021                    .as_ref()
1022                    .and_then(Weak::upgrade)
1023                    .expect("installed client");
1024                client.disconnect().await;
1025                self.events
1026                    .lock()
1027                    .unwrap_or_else(|poisoned| poisoned.into_inner())
1028                    .push("ready-finished");
1029                Ok(())
1030            })
1031        }
1032
1033        fn on_closed<'a>(&'a self, _scope: ConnectionScope) -> BoxFuture<'a, anyhow::Result<()>> {
1034            Box::pin(async move {
1035                self.events
1036                    .lock()
1037                    .unwrap_or_else(|poisoned| poisoned.into_inner())
1038                    .push("closed");
1039                Ok(())
1040            })
1041        }
1042
1043        fn shutdown(&self) -> BoxFuture<'_, anyhow::Result<()>> {
1044            Box::pin(async move {
1045                self.events
1046                    .lock()
1047                    .unwrap_or_else(|poisoned| poisoned.into_inner())
1048                    .push("shutdown");
1049                Ok(())
1050            })
1051        }
1052    }
1053
1054    struct BlockingShutdownLifecycle {
1055        started: async_channel::Sender<()>,
1056        release: async_channel::Receiver<()>,
1057        calls: AtomicUsize,
1058        completed: AtomicBool,
1059    }
1060
1061    struct QueuePressureLifecycle {
1062        ready_started: async_channel::Sender<()>,
1063        release_ready: async_channel::Receiver<()>,
1064        closed_calls: AtomicUsize,
1065        shutdown_calls: AtomicUsize,
1066    }
1067
1068    impl ClientLifecycle for QueuePressureLifecycle {
1069        fn on_ready<'a>(&'a self, _scope: ConnectionScope) -> BoxFuture<'a, anyhow::Result<()>> {
1070            Box::pin(async move {
1071                let _ = self.ready_started.try_send(());
1072                let _ = self.release_ready.recv().await;
1073                Ok(())
1074            })
1075        }
1076
1077        fn on_closed<'a>(&'a self, _scope: ConnectionScope) -> BoxFuture<'a, anyhow::Result<()>> {
1078            Box::pin(async move {
1079                self.closed_calls.fetch_add(1, Ordering::SeqCst);
1080                Ok(())
1081            })
1082        }
1083
1084        fn shutdown(&self) -> BoxFuture<'_, anyhow::Result<()>> {
1085            Box::pin(async move {
1086                self.shutdown_calls.fetch_add(1, Ordering::SeqCst);
1087                Ok(())
1088            })
1089        }
1090    }
1091
1092    impl ClientLifecycle for BlockingShutdownLifecycle {
1093        fn shutdown(&self) -> BoxFuture<'_, anyhow::Result<()>> {
1094            Box::pin(async move {
1095                self.calls.fetch_add(1, Ordering::SeqCst);
1096                let _ = self.started.try_send(());
1097                let _ = self.release.recv().await;
1098                self.completed.store(true, Ordering::Release);
1099                Ok(())
1100            })
1101        }
1102    }
1103
1104    #[derive(Default)]
1105    struct EarlyShutdownLifecycle {
1106        signalled: AtomicBool,
1107        shutdowns: AtomicUsize,
1108    }
1109
1110    impl ClientLifecycle for EarlyShutdownLifecycle {
1111        fn signal_shutdown(&self) {
1112            self.signalled.store(true, Ordering::Release);
1113        }
1114
1115        fn shutdown(&self) -> BoxFuture<'_, anyhow::Result<()>> {
1116            Box::pin(async move {
1117                assert!(self.signalled.load(Ordering::Acquire));
1118                self.shutdowns.fetch_add(1, Ordering::SeqCst);
1119                Ok(())
1120            })
1121        }
1122    }
1123
1124    #[derive(Default)]
1125    struct SynchronousPanicLifecycle {
1126        ready_calls: AtomicUsize,
1127        closed_calls: AtomicUsize,
1128        shutdown_calls: AtomicUsize,
1129    }
1130
1131    #[derive(Default)]
1132    struct DropPanickingFutureLifecycle {
1133        closed_calls: AtomicUsize,
1134        shutdown_calls: AtomicUsize,
1135    }
1136
1137    struct DropPanickingPendingFuture;
1138
1139    impl Future for DropPanickingPendingFuture {
1140        type Output = anyhow::Result<()>;
1141
1142        fn poll(
1143            self: std::pin::Pin<&mut Self>,
1144            _context: &mut std::task::Context<'_>,
1145        ) -> std::task::Poll<Self::Output> {
1146            std::task::Poll::Pending
1147        }
1148    }
1149
1150    impl Drop for DropPanickingPendingFuture {
1151        fn drop(&mut self) {
1152            panic!("injected lifecycle callback drop panic");
1153        }
1154    }
1155
1156    impl ClientLifecycle for SynchronousPanicLifecycle {
1157        fn on_ready<'a>(&'a self, _scope: ConnectionScope) -> BoxFuture<'a, anyhow::Result<()>> {
1158            self.ready_calls.fetch_add(1, Ordering::SeqCst);
1159            panic!("synchronous on_ready panic");
1160        }
1161
1162        fn on_closed<'a>(&'a self, _scope: ConnectionScope) -> BoxFuture<'a, anyhow::Result<()>> {
1163            self.closed_calls.fetch_add(1, Ordering::SeqCst);
1164            panic!("synchronous on_closed panic");
1165        }
1166
1167        fn shutdown(&self) -> BoxFuture<'_, anyhow::Result<()>> {
1168            self.shutdown_calls.fetch_add(1, Ordering::SeqCst);
1169            panic!("synchronous shutdown panic");
1170        }
1171    }
1172
1173    impl ClientLifecycle for DropPanickingFutureLifecycle {
1174        fn on_ready<'a>(&'a self, _scope: ConnectionScope) -> BoxFuture<'a, anyhow::Result<()>> {
1175            Box::pin(DropPanickingPendingFuture)
1176        }
1177
1178        fn on_closed<'a>(&'a self, _scope: ConnectionScope) -> BoxFuture<'a, anyhow::Result<()>> {
1179            Box::pin(async move {
1180                self.closed_calls.fetch_add(1, Ordering::SeqCst);
1181                Ok(())
1182            })
1183        }
1184
1185        fn shutdown(&self) -> BoxFuture<'_, anyhow::Result<()>> {
1186            Box::pin(async move {
1187                self.shutdown_calls.fetch_add(1, Ordering::SeqCst);
1188                Ok(())
1189            })
1190        }
1191    }
1192
1193    struct LogoutOrderHandler {
1194        lifecycle: Arc<RecordingLifecycle>,
1195    }
1196
1197    impl wacore::types::events::EventHandler for LogoutOrderHandler {
1198        fn handle_event(&self, event: Arc<wacore::types::events::Event>) {
1199            if matches!(&*event, wacore::types::events::Event::LoggedOut(_)) {
1200                self.lifecycle
1201                    .events
1202                    .lock()
1203                    .unwrap_or_else(|poisoned| poisoned.into_inner())
1204                    .push("logged-out".to_string());
1205            }
1206        }
1207
1208        fn interest(&self) -> wacore::types::events::EventInterest {
1209            wacore::types::events::EventInterest::of(&[wacore::types::events::EventKind::LoggedOut])
1210        }
1211    }
1212
1213    impl ClientLifecycle for BlockingReadyLifecycle {
1214        fn on_ready<'a>(&'a self, scope: ConnectionScope) -> BoxFuture<'a, anyhow::Result<()>> {
1215            Box::pin(async move {
1216                *self
1217                    .scope
1218                    .lock()
1219                    .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(scope);
1220                self.events
1221                    .lock()
1222                    .unwrap_or_else(|poisoned| poisoned.into_inner())
1223                    .push("ready-started");
1224                let _ = self.ready_started.try_send(());
1225                let _ = self.release_ready.recv().await;
1226                self.events
1227                    .lock()
1228                    .unwrap_or_else(|poisoned| poisoned.into_inner())
1229                    .push("ready-finished");
1230                Ok(())
1231            })
1232        }
1233
1234        fn on_closed<'a>(&'a self, _scope: ConnectionScope) -> BoxFuture<'a, anyhow::Result<()>> {
1235            Box::pin(async move {
1236                self.events
1237                    .lock()
1238                    .unwrap_or_else(|poisoned| poisoned.into_inner())
1239                    .push("closed");
1240                Ok(())
1241            })
1242        }
1243    }
1244
1245    #[test]
1246    fn scope_state_machine_is_sticky_and_cancellable() {
1247        let scope = ConnectionScope::new(41);
1248        let cancellation = scope.cancellation_signal();
1249
1250        assert_eq!(scope.state(), ConnectionScopeState::Open);
1251        assert!(scope.mark_ready());
1252        assert_eq!(scope.state(), ConnectionScopeState::Ready);
1253        scope.cancel();
1254        assert_eq!(scope.state(), ConnectionScopeState::Cancelled);
1255        assert!(cancellation.is_fired());
1256        scope.cancel();
1257        scope.close();
1258        assert_eq!(scope.state(), ConnectionScopeState::Closed);
1259    }
1260
1261    #[tokio::test]
1262    async fn terminal_signal_rejects_ready_publication() {
1263        let registration = Arc::new(LifecycleRegistration::new(
1264            Arc::new(RecordingLifecycle::default()),
1265            Arc::new(TokioRuntime),
1266        ));
1267        const GENERATION: u64 = 43;
1268        assert!(registration.begin_scope_if_current(GENERATION, || true));
1269        assert!(registration.ready(GENERATION).await);
1270        registration.signal_shutdown_sync();
1271
1272        let published = AtomicBool::new(false);
1273        assert!(!registration.publish_ready(GENERATION, || {
1274            published.store(true, Ordering::Release);
1275        }));
1276        assert!(!published.load(Ordering::Acquire));
1277    }
1278
1279    #[test]
1280    fn terminal_signal_waits_for_construction_commit() {
1281        let registration = Arc::new(LifecycleRegistration::new(
1282            Arc::new(RecordingLifecycle::default()),
1283            Arc::new(TokioRuntime),
1284        ));
1285        let (commit_started_tx, commit_started_rx) = std::sync::mpsc::sync_channel(1);
1286        let (release_commit_tx, release_commit_rx) = std::sync::mpsc::sync_channel(1);
1287        let (activation_tx, activation_rx) = std::sync::mpsc::sync_channel(1);
1288        let activation_registration = registration.clone();
1289        let activation = std::thread::spawn(move || {
1290            let activated = activation_registration.activate_with(|| {
1291                commit_started_tx.send(()).expect("publish commit start");
1292                release_commit_rx.recv().expect("release publish commit");
1293                true
1294            });
1295            activation_tx.send(activated).expect("activation result");
1296        });
1297        commit_started_rx
1298            .recv_timeout(Duration::from_secs(2))
1299            .expect("construction commit started");
1300
1301        let (shutdown_tx, shutdown_rx) = std::sync::mpsc::sync_channel(1);
1302        let shutdown_registration = registration.clone();
1303        let shutdown = std::thread::spawn(move || {
1304            shutdown_registration.signal_shutdown_sync();
1305            shutdown_tx.send(()).expect("shutdown result");
1306        });
1307        assert!(
1308            shutdown_rx
1309                .recv_timeout(Duration::from_millis(100))
1310                .is_err()
1311        );
1312
1313        release_commit_tx.send(()).expect("finish publish commit");
1314        assert!(
1315            activation_rx
1316                .recv_timeout(Duration::from_secs(2))
1317                .expect("construction activated")
1318        );
1319        shutdown_rx
1320            .recv_timeout(Duration::from_secs(2))
1321            .expect("terminal signal completed");
1322        activation.join().expect("activation thread");
1323        shutdown.join().expect("shutdown thread");
1324        assert_eq!(
1325            registration.construction_state.load(Ordering::Acquire),
1326            CONSTRUCTION_ACTIVE
1327        );
1328        assert!(registration.terminal.load(Ordering::Acquire));
1329    }
1330
1331    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1332    async fn terminal_cancellation_waits_for_ready_publication() {
1333        let registration = Arc::new(LifecycleRegistration::new(
1334            Arc::new(RecordingLifecycle::default()),
1335            Arc::new(TokioRuntime),
1336        ));
1337        const GENERATION: u64 = 47;
1338        assert!(registration.begin_scope_if_current(GENERATION, || true));
1339        assert!(registration.ready(GENERATION).await);
1340        let scope = registration
1341            .scope_for(GENERATION)
1342            .expect("ready connection scope");
1343
1344        let (started_tx, started_rx) = std::sync::mpsc::sync_channel(1);
1345        let (release_tx, release_rx) = std::sync::mpsc::sync_channel(1);
1346        let (published_tx, published_rx) = std::sync::mpsc::sync_channel(1);
1347        let publish_registration = registration.clone();
1348        let publish = std::thread::spawn(move || {
1349            let published = publish_registration.publish_ready(GENERATION, || {
1350                let _ = started_tx.send(());
1351                let _ = release_rx.recv();
1352            });
1353            let _ = published_tx.send(published);
1354        });
1355        started_rx
1356            .recv_timeout(Duration::from_secs(2))
1357            .expect("ready publication started");
1358
1359        let (attempted_tx, attempted_rx) = std::sync::mpsc::sync_channel(1);
1360        let (cancelled_tx, cancelled_rx) = std::sync::mpsc::sync_channel(1);
1361        let cancel_registration = registration.clone();
1362        let cancel = std::thread::spawn(move || {
1363            let _ = attempted_tx.send(());
1364            cancel_registration.signal_shutdown_sync();
1365            let _ = cancelled_tx.send(());
1366        });
1367        attempted_rx
1368            .recv_timeout(Duration::from_secs(2))
1369            .expect("terminal cancellation attempted");
1370        assert!(
1371            cancelled_rx
1372                .recv_timeout(Duration::from_millis(100))
1373                .is_err()
1374        );
1375
1376        release_tx.send(()).expect("release ready publication");
1377        assert!(
1378            published_rx
1379                .recv_timeout(Duration::from_secs(2))
1380                .expect("ready publication completed")
1381        );
1382        cancelled_rx
1383            .recv_timeout(Duration::from_secs(2))
1384            .expect("terminal cancellation completed");
1385        publish.join().expect("publication thread");
1386        cancel.join().expect("cancellation thread");
1387        assert_eq!(scope.state(), ConnectionScopeState::Cancelled);
1388    }
1389
1390    #[tokio::test]
1391    async fn ready_publication_allows_reentrant_terminal_signal() {
1392        let registration = Arc::new(LifecycleRegistration::new(
1393            Arc::new(RecordingLifecycle::default()),
1394            Arc::new(TokioRuntime),
1395        ));
1396        const GENERATION: u64 = 53;
1397        assert!(registration.begin_scope_if_current(GENERATION, || true));
1398        assert!(registration.ready(GENERATION).await);
1399        let scope = registration
1400            .scope_for(GENERATION)
1401            .expect("ready connection scope");
1402
1403        let (completed_tx, completed_rx) = std::sync::mpsc::sync_channel(1);
1404        let publish_registration = registration.clone();
1405        let signal_registration = registration.clone();
1406        let publish = std::thread::spawn(move || {
1407            let published = publish_registration.publish_ready(GENERATION, || {
1408                signal_registration.signal_shutdown_sync();
1409            });
1410            let _ = completed_tx.send(published);
1411        });
1412
1413        assert!(
1414            completed_rx
1415                .recv_timeout(Duration::from_secs(2))
1416                .expect("reentrant terminal signal completed")
1417        );
1418        publish.join().expect("publication thread");
1419        assert_eq!(scope.state(), ConnectionScopeState::Cancelled);
1420    }
1421
1422    #[tokio::test]
1423    async fn cleanup_cancels_before_io_and_closes_after_authoritative_teardown() {
1424        let persistence_manager = Arc::new(
1425            PersistenceManager::new(crate::test_utils::create_test_backend().await)
1426                .await
1427                .expect("persistence manager"),
1428        );
1429        let lifecycle = Arc::new(RecordingLifecycle::default());
1430        let build = Client::builder()
1431            .with_runtime(TokioRuntime)
1432            .with_persistence_manager(persistence_manager)
1433            .with_transport_factory(MockTransportFactory::new())
1434            .with_http_client(MockHttpClient)
1435            .with_lifecycle_arc(lifecycle.clone())
1436            .build()
1437            .await
1438            .expect("client build");
1439        let client = build.into_client();
1440        const GENERATION: u64 = 9;
1441        client
1442            .connection_generation
1443            .store(GENERATION, Ordering::SeqCst);
1444        let registration = client.lifecycle.as_ref().expect("lifecycle registration");
1445        assert!(registration.begin_scope_if_current(GENERATION, || true));
1446        client.dispatch_connected(GENERATION).await;
1447
1448        let scope = lifecycle
1449            .scopes
1450            .lock()
1451            .unwrap_or_else(|poisoned| poisoned.into_inner())
1452            .first()
1453            .cloned()
1454            .expect("ready scope");
1455        let cancelled = scope.cancellation_signal();
1456        let (started_tx, started_rx) = async_channel::bounded(1);
1457        let (release_tx, release_rx) = async_channel::bounded(1);
1458        *client.transport.lock().await = Some(Arc::new(BlockingDisconnect {
1459            started: started_tx,
1460            release: release_rx,
1461        }));
1462
1463        let cleanup_client = Arc::clone(&client);
1464        let cleanup = tokio::spawn(async move {
1465            cleanup_client.cleanup_connection_state().await;
1466        });
1467        tokio::time::timeout(Duration::from_secs(2), started_rx.recv())
1468            .await
1469            .expect("transport cleanup started")
1470            .expect("transport remained alive");
1471
1472        assert_eq!(scope.state(), ConnectionScopeState::Cancelled);
1473        assert!(cancelled.is_fired());
1474        assert_eq!(lifecycle.events(), vec!["install", "ready:9"]);
1475
1476        release_tx.send(()).await.expect("release cleanup");
1477        tokio::time::timeout(Duration::from_secs(2), cleanup)
1478            .await
1479            .expect("cleanup completed")
1480            .expect("cleanup did not panic");
1481
1482        assert_eq!(scope.state(), ConnectionScopeState::Closed);
1483        client.shutdown_lifecycle().await;
1484        client.shutdown_lifecycle().await;
1485        assert_eq!(lifecycle.shutdowns.load(Ordering::SeqCst), 1);
1486        assert_eq!(
1487            lifecycle.events(),
1488            vec!["install", "ready:9", "closed:9", "shutdown"]
1489        );
1490        client.signal_shutdown_sync();
1491    }
1492
1493    #[tokio::test]
1494    async fn disconnect_requests_lifecycle_shutdown_before_cancellable_io() {
1495        let persistence_manager = Arc::new(
1496            PersistenceManager::new(crate::test_utils::create_test_backend().await)
1497                .await
1498                .expect("persistence manager"),
1499        );
1500        let lifecycle = Arc::new(EarlyShutdownLifecycle::default());
1501        let client = Client::builder()
1502            .with_runtime(TokioRuntime)
1503            .with_persistence_manager(persistence_manager)
1504            .with_transport_factory(MockTransportFactory::new())
1505            .with_http_client(MockHttpClient)
1506            .with_lifecycle_arc(lifecycle.clone())
1507            .build()
1508            .await
1509            .expect("client build")
1510            .into_client();
1511        let (started_tx, started_rx) = async_channel::bounded(1);
1512        let (_release_tx, release_rx) = async_channel::bounded(1);
1513        *client.transport.lock().await = Some(Arc::new(BlockingDisconnect {
1514            started: started_tx,
1515            release: release_rx,
1516        }));
1517
1518        let disconnect_client = Arc::clone(&client);
1519        let disconnect = tokio::spawn(async move {
1520            disconnect_client.disconnect().await;
1521        });
1522        tokio::time::timeout(Duration::from_secs(2), started_rx.recv())
1523            .await
1524            .expect("disconnect reached cancellable transport I/O")
1525            .expect("transport remained alive");
1526        assert!(lifecycle.signalled.load(Ordering::Acquire));
1527
1528        disconnect.abort();
1529        let _ = disconnect.await;
1530        tokio::time::timeout(Duration::from_secs(2), async {
1531            while lifecycle.shutdowns.load(Ordering::SeqCst) != 1 {
1532                tokio::task::yield_now().await;
1533            }
1534        })
1535        .await
1536        .expect("detached lifecycle shutdown completed");
1537    }
1538
1539    #[tokio::test]
1540    async fn dropping_last_client_owner_signals_standalone_lifecycle() {
1541        let persistence_manager = Arc::new(
1542            PersistenceManager::new(crate::test_utils::create_test_backend().await)
1543                .await
1544                .expect("persistence manager"),
1545        );
1546        let lifecycle = Arc::new(EarlyShutdownLifecycle::default());
1547        let client = Client::builder()
1548            .with_runtime(TokioRuntime)
1549            .with_persistence_manager(persistence_manager)
1550            .with_transport_factory(MockTransportFactory::new())
1551            .with_http_client(MockHttpClient)
1552            .with_lifecycle_arc(lifecycle.clone())
1553            .build()
1554            .await
1555            .expect("client build")
1556            .into_client();
1557        let weak = Arc::downgrade(&client);
1558
1559        drop(client);
1560
1561        tokio::time::timeout(Duration::from_secs(2), async {
1562            while weak.upgrade().is_some() {
1563                tokio::task::yield_now().await;
1564            }
1565        })
1566        .await
1567        .expect("background services released the client");
1568        assert!(lifecycle.signalled.load(Ordering::Acquire));
1569    }
1570
1571    #[tokio::test]
1572    async fn cancellation_does_not_wait_for_a_running_callback() {
1573        let persistence_manager = Arc::new(
1574            PersistenceManager::new(crate::test_utils::create_test_backend().await)
1575                .await
1576                .expect("persistence manager"),
1577        );
1578        let (ready_started_tx, ready_started_rx) = async_channel::bounded(1);
1579        let (release_ready_tx, release_ready_rx) = async_channel::bounded(1);
1580        let lifecycle = Arc::new(BlockingReadyLifecycle {
1581            ready_started: ready_started_tx,
1582            release_ready: release_ready_rx,
1583            scope: std::sync::Mutex::new(None),
1584            events: std::sync::Mutex::new(Vec::new()),
1585        });
1586        let client = Client::builder()
1587            .with_runtime(TokioRuntime)
1588            .with_persistence_manager(persistence_manager)
1589            .with_transport_factory(MockTransportFactory::new())
1590            .with_http_client(MockHttpClient)
1591            .with_lifecycle_arc(lifecycle.clone())
1592            .build()
1593            .await
1594            .expect("client build")
1595            .into_client();
1596        const GENERATION: u64 = 13;
1597        client
1598            .connection_generation
1599            .store(GENERATION, Ordering::SeqCst);
1600        assert!(
1601            client
1602                .lifecycle
1603                .as_ref()
1604                .expect("lifecycle registration")
1605                .begin_scope_if_current(GENERATION, || true)
1606        );
1607
1608        let ready_client = Arc::clone(&client);
1609        let ready_task = tokio::spawn(async move {
1610            ready_client.dispatch_connected(GENERATION).await;
1611        });
1612        ready_started_rx
1613            .recv()
1614            .await
1615            .expect("ready callback started");
1616        let scope = lifecycle
1617            .scope
1618            .lock()
1619            .unwrap_or_else(|poisoned| poisoned.into_inner())
1620            .clone()
1621            .expect("ready scope");
1622
1623        let cleanup_client = Arc::clone(&client);
1624        let cleanup_task = tokio::spawn(async move {
1625            cleanup_client.cleanup_connection_state().await;
1626        });
1627        tokio::time::timeout(Duration::from_secs(2), async {
1628            while !scope.is_cancelled() {
1629                tokio::task::yield_now().await;
1630            }
1631        })
1632        .await
1633        .expect("scope cancellation");
1634        tokio::time::timeout(Duration::from_secs(2), cleanup_task)
1635            .await
1636            .expect("cleanup completed while callback was blocked")
1637            .expect("cleanup task did not panic");
1638        assert_eq!(scope.state(), ConnectionScopeState::Closed);
1639
1640        release_ready_tx.send(()).await.expect("release ready hook");
1641        ready_task.await.expect("ready task did not panic");
1642        client.shutdown_lifecycle().await;
1643        assert_eq!(
1644            *lifecycle
1645                .events
1646                .lock()
1647                .unwrap_or_else(|poisoned| poisoned.into_inner()),
1648            vec!["ready-started", "ready-finished", "closed"]
1649        );
1650        client.signal_shutdown_sync();
1651    }
1652
1653    #[tokio::test]
1654    async fn ready_callback_can_disconnect_its_client() {
1655        let persistence_manager = Arc::new(
1656            PersistenceManager::new(crate::test_utils::create_test_backend().await)
1657                .await
1658                .expect("persistence manager"),
1659        );
1660        let lifecycle = Arc::new(ReentrantDisconnectLifecycle::default());
1661        let client = Client::builder()
1662            .with_runtime(TokioRuntime)
1663            .with_persistence_manager(persistence_manager)
1664            .with_transport_factory(MockTransportFactory::new())
1665            .with_http_client(MockHttpClient)
1666            .with_lifecycle_arc(lifecycle.clone())
1667            .build()
1668            .await
1669            .expect("client build")
1670            .into_client();
1671        const GENERATION: u64 = 17;
1672        client
1673            .connection_generation
1674            .store(GENERATION, Ordering::SeqCst);
1675        assert!(
1676            client
1677                .lifecycle
1678                .as_ref()
1679                .expect("lifecycle registration")
1680                .begin_scope_if_current(GENERATION, || true)
1681        );
1682
1683        tokio::time::timeout(
1684            Duration::from_secs(2),
1685            client.dispatch_connected(GENERATION),
1686        )
1687        .await
1688        .expect("reentrant disconnect completed");
1689        client.shutdown_lifecycle().await;
1690
1691        assert_eq!(
1692            *lifecycle
1693                .events
1694                .lock()
1695                .unwrap_or_else(|poisoned| poisoned.into_inner()),
1696            vec!["ready-started", "ready-finished", "closed", "shutdown"]
1697        );
1698        assert!(!client.is_logged_in());
1699        assert!(!client.is_ready.load(Ordering::Relaxed));
1700    }
1701
1702    #[tokio::test]
1703    async fn ready_callback_reconnect_requests_retire_the_scope_before_returning() {
1704        for immediate in [false, true] {
1705            let persistence_manager = Arc::new(
1706                PersistenceManager::new(crate::test_utils::create_test_backend().await)
1707                    .await
1708                    .expect("persistence manager"),
1709            );
1710            let lifecycle = Arc::new(ReentrantReconnectLifecycle {
1711                client: std::sync::Mutex::new(None),
1712                events: std::sync::Mutex::new(Vec::new()),
1713                immediate,
1714            });
1715            let client = Client::builder()
1716                .with_runtime(TokioRuntime)
1717                .with_persistence_manager(persistence_manager)
1718                .with_transport_factory(MockTransportFactory::new())
1719                .with_http_client(MockHttpClient)
1720                .with_lifecycle_arc(lifecycle.clone())
1721                .build()
1722                .await
1723                .expect("client build")
1724                .into_client();
1725            const GENERATION: u64 = 18;
1726            client
1727                .connection_generation
1728                .store(GENERATION, Ordering::SeqCst);
1729            let registration = client.lifecycle.as_ref().expect("lifecycle registration");
1730            assert!(registration.begin_scope_if_current(GENERATION, || true));
1731
1732            tokio::time::timeout(
1733                Duration::from_secs(2),
1734                client.dispatch_connected(GENERATION),
1735            )
1736            .await
1737            .expect("reentrant reconnect completed");
1738            let scope = registration
1739                .scope_for(GENERATION)
1740                .expect("cancelled connection scope");
1741            assert_eq!(scope.state(), ConnectionScopeState::Cancelled);
1742            assert!(!client.is_ready.load(Ordering::Relaxed));
1743
1744            registration.close_scope(GENERATION);
1745            registration.shutdown().await;
1746            assert_eq!(
1747                *lifecycle
1748                    .events
1749                    .lock()
1750                    .unwrap_or_else(|poisoned| poisoned.into_inner()),
1751                vec!["ready-started", "ready-finished", "closed", "shutdown"]
1752            );
1753        }
1754    }
1755
1756    #[tokio::test]
1757    async fn callback_timeout_does_not_hold_connection_cleanup() {
1758        let (ready_started_tx, ready_started_rx) = async_channel::bounded(1);
1759        let (_release_ready_tx, release_ready_rx) = async_channel::bounded(1);
1760        let lifecycle = Arc::new(BlockingReadyLifecycle {
1761            ready_started: ready_started_tx,
1762            release_ready: release_ready_rx,
1763            scope: std::sync::Mutex::new(None),
1764            events: std::sync::Mutex::new(Vec::new()),
1765        });
1766        let registration = Arc::new(LifecycleRegistration::new_with_timeout(
1767            lifecycle.clone(),
1768            Arc::new(TokioRuntime),
1769            Duration::from_millis(20),
1770        ));
1771        const GENERATION: u64 = 19;
1772        assert!(registration.begin_scope_if_current(GENERATION, || true));
1773
1774        let ready_registration = Arc::clone(&registration);
1775        let ready = tokio::spawn(async move { ready_registration.ready(GENERATION).await });
1776        ready_started_rx
1777            .recv()
1778            .await
1779            .expect("ready callback started");
1780        let scope = lifecycle
1781            .scope
1782            .lock()
1783            .unwrap_or_else(|poisoned| poisoned.into_inner())
1784            .clone()
1785            .expect("ready scope");
1786
1787        registration.cancel_scope(GENERATION);
1788        registration.close_scope(GENERATION);
1789        assert_eq!(scope.state(), ConnectionScopeState::Closed);
1790        assert!(
1791            !tokio::time::timeout(Duration::from_secs(1), ready)
1792                .await
1793                .expect("ready callback was bounded")
1794                .expect("ready task did not panic")
1795        );
1796        tokio::time::timeout(Duration::from_secs(1), registration.shutdown())
1797            .await
1798            .expect("lifecycle shutdown completed");
1799
1800        assert_eq!(
1801            *lifecycle
1802                .events
1803                .lock()
1804                .unwrap_or_else(|poisoned| poisoned.into_inner()),
1805            vec!["ready-started", "closed"]
1806        );
1807    }
1808
1809    #[test]
1810    fn callback_queue_retains_latest_ready_with_lossless_close_backlog() {
1811        let mut queue = CallbackQueue::default();
1812        for generation in 1..=CALLBACK_QUEUE_TARGET_CAPACITY as u64 {
1813            let scope = ConnectionScope::new(generation);
1814            scope.close();
1815            assert!(
1816                queue
1817                    .push_with_pressure_policy(LifecycleCallback::Closed(scope))
1818                    .is_empty()
1819            );
1820        }
1821
1822        let (first_done, _first_completion) = async_channel::bounded(1);
1823        assert!(
1824            queue
1825                .push_with_pressure_policy(LifecycleCallback::Ready {
1826                    scope: ConnectionScope::new(100),
1827                    done: first_done,
1828                })
1829                .is_empty()
1830        );
1831        assert_eq!(queue.pending.len(), CALLBACK_QUEUE_TARGET_CAPACITY + 1);
1832
1833        let (latest_done, _latest_completion) = async_channel::bounded(1);
1834        let mut dropped = queue.push_with_pressure_policy(LifecycleCallback::Ready {
1835            scope: ConnectionScope::new(101),
1836            done: latest_done,
1837        });
1838        assert_eq!(dropped.len(), 1);
1839        let dropped = dropped.pop().expect("older ready callback is replaceable");
1840        assert!(matches!(
1841            dropped,
1842            LifecycleCallback::Ready { scope, .. } if scope.generation() == 100
1843        ));
1844
1845        let extra_closed = ConnectionScope::new(102);
1846        extra_closed.close();
1847        assert!(
1848            queue
1849                .push_with_pressure_policy(LifecycleCallback::Closed(extra_closed))
1850                .is_empty()
1851        );
1852        assert_eq!(queue.pending.len(), CALLBACK_QUEUE_TARGET_CAPACITY + 2);
1853        assert_eq!(
1854            queue
1855                .pending
1856                .iter()
1857                .filter(|callback| matches!(callback, LifecycleCallback::Ready { .. }))
1858                .count(),
1859            1
1860        );
1861        assert!(queue.pending.iter().any(|callback| {
1862            matches!(callback, LifecycleCallback::Ready { scope, .. } if scope.generation() == 101)
1863        }));
1864    }
1865
1866    #[tokio::test]
1867    async fn callback_queue_preserves_every_scope_closure_before_shutdown() {
1868        let (ready_started_tx, ready_started_rx) = async_channel::bounded(1);
1869        let (release_ready_tx, release_ready_rx) = async_channel::bounded(1);
1870        let lifecycle = Arc::new(QueuePressureLifecycle {
1871            ready_started: ready_started_tx,
1872            release_ready: release_ready_rx,
1873            closed_calls: AtomicUsize::new(0),
1874            shutdown_calls: AtomicUsize::new(0),
1875        });
1876        let registration = Arc::new(LifecycleRegistration::new_with_timeout(
1877            lifecycle.clone(),
1878            Arc::new(TokioRuntime),
1879            Duration::from_secs(1),
1880        ));
1881
1882        let active_scope = ConnectionScope::new(1);
1883        assert!(active_scope.mark_ready());
1884        let (done, _done_rx) = async_channel::bounded(1);
1885        registration.enqueue_callback(LifecycleCallback::Ready {
1886            scope: active_scope,
1887            done,
1888        });
1889        ready_started_rx
1890            .recv()
1891            .await
1892            .expect("ready callback started");
1893
1894        let closed_callbacks = CALLBACK_QUEUE_TARGET_CAPACITY as u64 * 4 - 2;
1895        for generation in 2..(CALLBACK_QUEUE_TARGET_CAPACITY as u64 * 4) {
1896            let scope = ConnectionScope::new(generation);
1897            scope.close();
1898            registration.enqueue_callback(LifecycleCallback::Closed(scope));
1899
1900            let (done, _done_rx) = async_channel::bounded(1);
1901            registration.enqueue_callback(LifecycleCallback::Ready {
1902                scope: ConnectionScope::new(generation + closed_callbacks),
1903                done,
1904            });
1905        }
1906        assert_eq!(
1907            registration.callback_queue().pending.len(),
1908            usize::try_from(closed_callbacks + 1).expect("callback count fits usize")
1909        );
1910
1911        let shutdown_registration = registration.clone();
1912        let shutdown = tokio::spawn(async move { shutdown_registration.shutdown().await });
1913        tokio::time::timeout(Duration::from_secs(1), async {
1914            loop {
1915                let compacted = {
1916                    let queue = registration.callback_queue();
1917                    if queue.shutdown_enqueued {
1918                        assert_eq!(
1919                            queue.pending.len(),
1920                            usize::try_from(closed_callbacks + 1)
1921                                .expect("terminal callback count fits usize")
1922                        );
1923                        true
1924                    } else {
1925                        false
1926                    }
1927                };
1928                if compacted {
1929                    break;
1930                }
1931                tokio::task::yield_now().await;
1932            }
1933        })
1934        .await
1935        .expect("terminal backlog compaction");
1936
1937        release_ready_tx
1938            .send(())
1939            .await
1940            .expect("release ready callback");
1941        shutdown.await.expect("bounded terminal shutdown");
1942        assert_eq!(
1943            lifecycle.closed_calls.load(Ordering::SeqCst),
1944            usize::try_from(closed_callbacks).expect("closure count fits usize")
1945        );
1946        assert_eq!(lifecycle.shutdown_calls.load(Ordering::SeqCst), 1);
1947    }
1948
1949    #[tokio::test]
1950    async fn cancelled_shutdown_waiter_does_not_cancel_shutdown() {
1951        let (started_tx, started_rx) = async_channel::bounded(1);
1952        let (release_tx, release_rx) = async_channel::bounded(1);
1953        let lifecycle = Arc::new(BlockingShutdownLifecycle {
1954            started: started_tx,
1955            release: release_rx,
1956            calls: AtomicUsize::new(0),
1957            completed: AtomicBool::new(false),
1958        });
1959        let registration = Arc::new(LifecycleRegistration::new(
1960            lifecycle.clone(),
1961            Arc::new(TokioRuntime),
1962        ));
1963
1964        let first_registration = Arc::clone(&registration);
1965        let first = tokio::spawn(async move { first_registration.shutdown().await });
1966        started_rx.recv().await.expect("shutdown callback started");
1967        first.abort();
1968        let _ = first.await;
1969
1970        release_tx
1971            .send(())
1972            .await
1973            .expect("release shutdown callback");
1974        tokio::time::timeout(Duration::from_secs(2), registration.shutdown())
1975            .await
1976            .expect("later shutdown waiter observed completion");
1977
1978        assert!(lifecycle.completed.load(Ordering::Acquire));
1979        assert_eq!(lifecycle.calls.load(Ordering::SeqCst), 1);
1980    }
1981
1982    #[tokio::test]
1983    async fn synchronous_callback_panics_do_not_strand_the_driver() {
1984        let lifecycle = Arc::new(SynchronousPanicLifecycle::default());
1985        let registration = Arc::new(LifecycleRegistration::new(
1986            lifecycle.clone(),
1987            Arc::new(TokioRuntime),
1988        ));
1989        const GENERATION: u64 = 23;
1990        assert!(registration.begin_scope_if_current(GENERATION, || true));
1991
1992        assert!(registration.ready(GENERATION).await);
1993        registration.close_scope(GENERATION);
1994        tokio::time::timeout(Duration::from_secs(2), registration.shutdown())
1995            .await
1996            .expect("callback driver recovered from synchronous panics");
1997
1998        assert_eq!(lifecycle.ready_calls.load(Ordering::SeqCst), 1);
1999        assert_eq!(lifecycle.closed_calls.load(Ordering::SeqCst), 1);
2000        assert_eq!(lifecycle.shutdown_calls.load(Ordering::SeqCst), 1);
2001    }
2002
2003    #[tokio::test]
2004    async fn callback_drop_panics_do_not_strand_the_driver() {
2005        let lifecycle = Arc::new(DropPanickingFutureLifecycle::default());
2006        let registration = Arc::new(LifecycleRegistration::new_with_timeout(
2007            lifecycle.clone(),
2008            Arc::new(TokioRuntime),
2009            Duration::from_millis(10),
2010        ));
2011        const GENERATION: u64 = 24;
2012        assert!(registration.begin_scope_if_current(GENERATION, || true));
2013
2014        assert!(
2015            tokio::time::timeout(Duration::from_secs(1), registration.ready(GENERATION))
2016                .await
2017                .expect("ready callback cancellation completed")
2018        );
2019        registration.close_scope(GENERATION);
2020        tokio::time::timeout(Duration::from_secs(1), registration.shutdown())
2021            .await
2022            .expect("callback driver recovered from a drop panic");
2023
2024        assert_eq!(lifecycle.closed_calls.load(Ordering::SeqCst), 1);
2025        assert_eq!(lifecycle.shutdown_calls.load(Ordering::SeqCst), 1);
2026    }
2027
2028    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2029    async fn final_scope_closure_is_published_before_shutdown() {
2030        let lifecycle = Arc::new(RecordingLifecycle::default());
2031        let registration = Arc::new(LifecycleRegistration::new(
2032            lifecycle.clone(),
2033            Arc::new(TokioRuntime),
2034        ));
2035        const GENERATION: u64 = 29;
2036        assert!(registration.begin_scope_if_current(GENERATION, || true));
2037
2038        let removed = Arc::new(std::sync::Barrier::new(2));
2039        let release = Arc::new(std::sync::Barrier::new(2));
2040        let close_registration = Arc::clone(&registration);
2041        let close_removed = Arc::clone(&removed);
2042        let close_release = Arc::clone(&release);
2043        let close = tokio::task::spawn_blocking(move || {
2044            close_registration.close_scope_with(GENERATION, || {
2045                close_removed.wait();
2046                close_release.wait();
2047            });
2048        });
2049
2050        removed.wait();
2051        let shutdown_registration = Arc::clone(&registration);
2052        let shutdown = tokio::spawn(async move { shutdown_registration.shutdown().await });
2053        // `terminal` flips inside signal_shutdown_sync, right before it reaches for
2054        // the `scopes` lock the blocked close callback is holding. Once it is set,
2055        // shutdown provably cannot get past that lock until `release` is waited.
2056        crate::test_utils::poll_until("shutdown to reach the scope registry", || {
2057            registration.terminal.load(Ordering::Acquire)
2058        })
2059        .await;
2060        assert!(!shutdown.is_finished());
2061
2062        release.wait();
2063        close.await.expect("scope close task");
2064        shutdown.await.expect("shutdown task");
2065        assert_eq!(
2066            lifecycle.events(),
2067            vec!["closed:29".to_string(), "shutdown".to_string()]
2068        );
2069    }
2070
2071    #[tokio::test]
2072    async fn cancelled_cleanup_waiter_does_not_strand_its_scope() {
2073        let persistence_manager = Arc::new(
2074            PersistenceManager::new(crate::test_utils::create_test_backend().await)
2075                .await
2076                .expect("persistence manager"),
2077        );
2078        let lifecycle = Arc::new(RecordingLifecycle::default());
2079        let client = Client::builder()
2080            .with_runtime(TokioRuntime)
2081            .with_persistence_manager(persistence_manager)
2082            .with_transport_factory(MockTransportFactory::new())
2083            .with_http_client(MockHttpClient)
2084            .with_lifecycle_arc(lifecycle.clone())
2085            .build()
2086            .await
2087            .expect("client build")
2088            .into_client();
2089        const GENERATION: u64 = 37;
2090        client
2091            .connection_generation
2092            .store(GENERATION, Ordering::SeqCst);
2093        let registration = client.lifecycle.as_ref().expect("lifecycle registration");
2094        assert!(registration.begin_scope_if_current(GENERATION, || true));
2095        client.dispatch_connected(GENERATION).await;
2096        let scope = registration
2097            .scope_for(GENERATION)
2098            .expect("connection scope");
2099
2100        let (started_tx, started_rx) = async_channel::bounded(1);
2101        let (release_tx, release_rx) = async_channel::bounded(1);
2102        *client.transport.lock().await = Some(Arc::new(BlockingDisconnect {
2103            started: started_tx,
2104            release: release_rx,
2105        }));
2106
2107        let cleanup_client = Arc::clone(&client);
2108        let cleanup = tokio::spawn(async move {
2109            cleanup_client.cleanup_connection_state().await;
2110        });
2111        started_rx.recv().await.expect("cleanup reached transport");
2112        cleanup.abort();
2113        let _ = cleanup.await;
2114        assert_eq!(scope.state(), ConnectionScopeState::Cancelled);
2115
2116        release_tx.send(()).await.expect("release cleanup");
2117        tokio::time::timeout(Duration::from_secs(2), async {
2118            while scope.state() != ConnectionScopeState::Closed {
2119                tokio::task::yield_now().await;
2120            }
2121        })
2122        .await
2123        .expect("detached cleanup closed the scope");
2124        assert!(registration.scope_for(GENERATION).is_none());
2125
2126        registration.shutdown().await;
2127        assert_eq!(
2128            lifecycle.events(),
2129            vec!["install", "ready:37", "closed:37", "shutdown"]
2130        );
2131        client.signal_shutdown_sync();
2132    }
2133
2134    #[tokio::test]
2135    async fn detached_cleanup_propagates_panics_to_its_waiter() {
2136        let persistence_manager = Arc::new(
2137            PersistenceManager::new(crate::test_utils::create_test_backend().await)
2138                .await
2139                .expect("persistence manager"),
2140        );
2141        let lifecycle = Arc::new(RecordingLifecycle::default());
2142        let client = Client::builder()
2143            .with_runtime(TokioRuntime)
2144            .with_persistence_manager(persistence_manager)
2145            .with_transport_factory(MockTransportFactory::new())
2146            .with_http_client(MockHttpClient)
2147            .with_lifecycle_arc(lifecycle.clone())
2148            .build()
2149            .await
2150            .expect("client build")
2151            .into_client();
2152        const GENERATION: u64 = 41;
2153        client
2154            .connection_generation
2155            .store(GENERATION, Ordering::SeqCst);
2156        let registration = client.lifecycle.as_ref().expect("lifecycle registration");
2157        assert!(registration.begin_scope_if_current(GENERATION, || true));
2158        client.dispatch_connected(GENERATION).await;
2159        let scope = registration
2160            .scope_for(GENERATION)
2161            .expect("connection scope");
2162        *client.transport.lock().await = Some(Arc::new(PanickingDisconnect));
2163
2164        let cleanup_client = Arc::clone(&client);
2165        let cleanup = tokio::spawn(async move {
2166            cleanup_client.cleanup_connection_state().await;
2167        });
2168        let panic = tokio::time::timeout(Duration::from_secs(2), cleanup)
2169            .await
2170            .expect("cleanup waiter did not hang")
2171            .expect_err("cleanup panic should reach its waiter");
2172
2173        assert!(panic.is_panic());
2174        assert_eq!(scope.state(), ConnectionScopeState::Closed);
2175        assert!(registration.scope_for(GENERATION).is_none());
2176        tokio::time::timeout(Duration::from_secs(2), registration.shutdown())
2177            .await
2178            .expect("shutdown waited for the panicked cleanup scope");
2179        assert_eq!(
2180            lifecycle.events(),
2181            vec!["install", "ready:41", "closed:41", "shutdown"]
2182        );
2183        client.signal_shutdown_sync();
2184    }
2185
2186    #[tokio::test]
2187    async fn stale_generation_is_rejected_before_scope_publication() {
2188        let lifecycle = Arc::new(RecordingLifecycle::default());
2189        let registration = Arc::new(LifecycleRegistration::new(
2190            lifecycle.clone(),
2191            Arc::new(TokioRuntime),
2192        ));
2193        let generation = portable_atomic::AtomicU64::new(24);
2194        generation.store(25, Ordering::SeqCst);
2195
2196        assert!(
2197            !registration
2198                .begin_scope_if_current(24, || { generation.load(Ordering::SeqCst) == 24 })
2199        );
2200        assert!(registration.scope_for(24).is_none());
2201        tokio::time::timeout(Duration::from_secs(2), registration.shutdown())
2202            .await
2203            .expect("shutdown did not wait for a rejected scope");
2204        assert_eq!(lifecycle.shutdowns.load(Ordering::SeqCst), 1);
2205    }
2206
2207    #[tokio::test]
2208    async fn stale_connected_dispatch_cannot_claim_a_new_scope() {
2209        let persistence_manager = Arc::new(
2210            PersistenceManager::new(crate::test_utils::create_test_backend().await)
2211                .await
2212                .expect("persistence manager"),
2213        );
2214        let lifecycle = Arc::new(RecordingLifecycle::default());
2215        let client = Client::builder()
2216            .with_runtime(TokioRuntime)
2217            .with_persistence_manager(persistence_manager)
2218            .with_transport_factory(MockTransportFactory::new())
2219            .with_http_client(MockHttpClient)
2220            .with_lifecycle_arc(lifecycle.clone())
2221            .build()
2222            .await
2223            .expect("client build")
2224            .into_client();
2225        const STALE_GENERATION: u64 = 60;
2226        const CURRENT_GENERATION: u64 = 62;
2227        client
2228            .connection_generation
2229            .store(CURRENT_GENERATION, Ordering::SeqCst);
2230        let registration = client.lifecycle.as_ref().expect("lifecycle registration");
2231        assert!(registration.begin_scope_if_current(CURRENT_GENERATION, || true));
2232
2233        client.dispatch_connected(STALE_GENERATION).await;
2234
2235        let scope = registration
2236            .scope_for(CURRENT_GENERATION)
2237            .expect("current scope");
2238        assert_eq!(scope.state(), ConnectionScopeState::Open);
2239        assert!(!client.is_ready.load(Ordering::Relaxed));
2240        assert_eq!(lifecycle.events(), vec!["install"]);
2241
2242        client.dispatch_connected(CURRENT_GENERATION).await;
2243        assert_eq!(scope.state(), ConnectionScopeState::Ready);
2244        assert!(client.is_ready.load(Ordering::Relaxed));
2245        assert_eq!(lifecycle.events(), vec!["install", "ready:62"]);
2246
2247        registration.cancel_scope(CURRENT_GENERATION);
2248        registration.close_scope(CURRENT_GENERATION);
2249        registration.shutdown().await;
2250        client.signal_shutdown_sync();
2251    }
2252
2253    #[tokio::test]
2254    async fn rejected_success_restores_logged_out_state() {
2255        let persistence_manager = Arc::new(
2256            PersistenceManager::new(crate::test_utils::create_test_backend().await)
2257                .await
2258                .expect("persistence manager"),
2259        );
2260        let lifecycle = Arc::new(RecordingLifecycle::default());
2261        let client = Client::builder()
2262            .with_runtime(TokioRuntime)
2263            .with_persistence_manager(persistence_manager)
2264            .with_transport_factory(MockTransportFactory::new())
2265            .with_http_client(MockHttpClient)
2266            .with_lifecycle_arc(lifecycle)
2267            .build()
2268            .await
2269            .expect("client build")
2270            .into_client();
2271        client
2272            .lifecycle
2273            .as_ref()
2274            .expect("lifecycle registration")
2275            .signal_shutdown_sync();
2276
2277        let success = wacore_binary::builder::NodeBuilder::new("success").build();
2278        client.handle_success(&success.as_node_ref()).await;
2279
2280        assert!(!client.is_logged_in());
2281        assert_eq!(client.connection_generation.load(Ordering::SeqCst), 1);
2282    }
2283
2284    #[tokio::test]
2285    async fn replaced_scope_stays_closeable_by_its_generation() {
2286        let lifecycle = Arc::new(RecordingLifecycle::default());
2287        let registration = Arc::new(LifecycleRegistration::new(
2288            lifecycle.clone(),
2289            Arc::new(TokioRuntime),
2290        ));
2291
2292        assert!(registration.begin_scope_if_current(31, || true));
2293        assert!(registration.ready(31).await);
2294        let first = registration.scope_for(31).expect("first scope");
2295
2296        assert!(registration.begin_scope_if_current(32, || true));
2297        assert_eq!(first.state(), ConnectionScopeState::Cancelled);
2298        assert!(registration.scope_for(31).is_some());
2299        assert!(registration.ready(32).await);
2300
2301        registration.close_scope(31);
2302        assert_eq!(first.state(), ConnectionScopeState::Closed);
2303        assert!(registration.scope_for(31).is_none());
2304        assert!(registration.scope_for(32).is_some());
2305
2306        registration.close_scope(32);
2307        registration.shutdown().await;
2308        assert_eq!(
2309            lifecycle.events(),
2310            vec!["ready:31", "ready:32", "closed:31", "closed:32", "shutdown",]
2311        );
2312    }
2313
2314    #[tokio::test]
2315    async fn shutdown_waits_for_the_active_scope_and_is_terminal() {
2316        let lifecycle = Arc::new(RecordingLifecycle::default());
2317        let registration = Arc::new(LifecycleRegistration::new(
2318            lifecycle.clone(),
2319            Arc::new(TokioRuntime),
2320        ));
2321        const GENERATION: u64 = 21;
2322        assert!(registration.begin_scope_if_current(GENERATION, || true));
2323        let scope = registration
2324            .scope_for(GENERATION)
2325            .expect("active connection scope");
2326
2327        let shutdown_registration = Arc::clone(&registration);
2328        let shutdown = tokio::spawn(async move {
2329            shutdown_registration.shutdown().await;
2330        });
2331        tokio::time::timeout(Duration::from_secs(2), async {
2332            while !scope.is_cancelled() {
2333                tokio::task::yield_now().await;
2334            }
2335        })
2336        .await
2337        .expect("scope cancellation");
2338        assert!(!shutdown.is_finished());
2339
2340        registration.close_scope(GENERATION);
2341        shutdown.await.expect("shutdown did not panic");
2342        assert_eq!(
2343            lifecycle.events(),
2344            vec!["closed:21".to_string(), "shutdown".to_string()]
2345        );
2346
2347        assert!(!registration.begin_scope_if_current(GENERATION + 1, || true));
2348        assert!(registration.scope_for(GENERATION + 1).is_none());
2349        assert!(!registration.ready(GENERATION + 1).await);
2350        registration.shutdown().await;
2351        assert_eq!(lifecycle.shutdowns.load(Ordering::SeqCst), 1);
2352    }
2353
2354    #[tokio::test]
2355    async fn logout_event_precedes_terminal_lifecycle_shutdown() {
2356        let persistence_manager = Arc::new(
2357            PersistenceManager::new(crate::test_utils::create_test_backend().await)
2358                .await
2359                .expect("persistence manager"),
2360        );
2361        let lifecycle = Arc::new(RecordingLifecycle::default());
2362        let client = Client::builder()
2363            .with_runtime(TokioRuntime)
2364            .with_persistence_manager(persistence_manager)
2365            .with_transport_factory(MockTransportFactory::new())
2366            .with_http_client(MockHttpClient)
2367            .with_lifecycle_arc(lifecycle.clone())
2368            .build()
2369            .await
2370            .expect("client build")
2371            .into_client();
2372        client
2373            .subscribe_handler(Arc::new(LogoutOrderHandler {
2374                lifecycle: lifecycle.clone(),
2375            }))
2376            .detach();
2377
2378        client.logout().await;
2379
2380        assert_eq!(
2381            lifecycle.events(),
2382            vec!["install", "logged-out", "shutdown"]
2383        );
2384    }
2385}