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#[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#[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 pub fn cancellation_signal(&self) -> ShutdownSignal {
79 self.inner.cancellation.subscribe()
80 }
81
82 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
130pub 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 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 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 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 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(®istration);
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(®istration);
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(®istration);
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(®istration);
2052 let shutdown = tokio::spawn(async move { shutdown_registration.shutdown().await });
2053 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(®istration);
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}