1use crate::errors::{Error, Result};
4use crate::realtime::{Client, ReconnectBackoff, TypedSubscription, lock_unpoisoned};
5use futures_util::future::BoxFuture;
6use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
7use std::sync::{Arc, Mutex};
8use tokio::sync::{Mutex as AsyncMutex, watch};
9use tokio::time::Duration;
10
11const MAX_REQUEST_REFRESH_ATTEMPTS: usize = 3;
12const REQUEST_REFRESH_INITIAL_BACKOFF: Duration = Duration::from_millis(50);
13const REQUEST_REFRESH_MAX_BACKOFF: Duration = Duration::from_millis(200);
14
15type FetchSnapshotFn<TSnapshot> =
16 Arc<dyn Fn() -> BoxFuture<'static, Result<TSnapshot>> + Send + Sync>;
17type ApplySnapshotFn<TSnapshot, TPublication> =
18 Arc<dyn Fn(TSnapshot, Vec<TPublication>) + Send + Sync>;
19type ApplyLiveFn<TPublication> = Arc<dyn Fn(Vec<TPublication>) + Send + Sync>;
20type ReadPublicationFn<TPublication> = Arc<dyn Fn(TPublication) -> Vec<TPublication> + Send + Sync>;
21type DecodeFn<TPublication> = Arc<dyn Fn(&[u8]) -> Result<TPublication> + Send + Sync>;
22type NotifyFn = Arc<dyn Fn() + Send + Sync>;
23pub type SnapshotErrorFn = Arc<dyn Fn(Error) + Send + Sync>;
26
27pub struct SnapshotThenStreamConfig<TSnapshot, TPublication> {
29 pub client: Client,
30 pub channel: String,
31 pub decode: DecodeFn<TPublication>,
32 pub fetch_snapshot: FetchSnapshotFn<TSnapshot>,
33 pub read_publication: ReadPublicationFn<TPublication>,
34 pub apply_snapshot: ApplySnapshotFn<TSnapshot, TPublication>,
35 pub apply_live_publications: ApplyLiveFn<TPublication>,
36 pub max_buffered: usize,
37 pub on_reconnect: Option<NotifyFn>,
38 pub on_snapshot_refresh: Option<NotifyFn>,
39 pub on_error: Option<SnapshotErrorFn>,
40}
41
42pub struct SnapshotThenStream<TSnapshot, TPublication> {
44 inner: Arc<Inner<TSnapshot, TPublication>>,
45 counts_handle: bool,
46}
47
48struct Inner<TSnapshot, TPublication> {
49 client: Client,
50 channel: String,
51 decode: DecodeFn<TPublication>,
52 fetch_snapshot: FetchSnapshotFn<TSnapshot>,
53 read_publication: ReadPublicationFn<TPublication>,
54 apply_snapshot: ApplySnapshotFn<TSnapshot, TPublication>,
55 apply_live_publications: ApplyLiveFn<TPublication>,
56 max_buffered: usize,
57 on_reconnect: Option<NotifyFn>,
58 on_snapshot_refresh: Option<NotifyFn>,
59 on_error: Mutex<Option<SnapshotErrorFn>>,
60 ready: AtomicBool,
61 disposed: AtomicBool,
62 generation: AtomicU64,
63 publications: Mutex<PublicationState<TPublication>>,
64 refresh_gate: AsyncMutex<()>,
65 refresh_worker_running: AtomicBool,
66 refresh_requested: AtomicBool,
67 last_error: Mutex<Option<Error>>,
68 stop_tx: watch::Sender<bool>,
69 connection_tx: watch::Sender<Option<Result<()>>>,
70 started: AtomicBool,
71 connected_once: AtomicBool,
72 handles: AtomicUsize,
73}
74
75enum PublicationState<TPublication> {
76 Buffering(Vec<TPublication>),
77 Ready,
78}
79
80impl<TSnapshot, TPublication> SnapshotThenStream<TSnapshot, TPublication>
81where
82 TSnapshot: Send + 'static,
83 TPublication: Send + 'static,
84{
85 pub fn new(cfg: SnapshotThenStreamConfig<TSnapshot, TPublication>) -> Self {
86 let max_buffered = if cfg.max_buffered == 0 {
87 200
88 } else {
89 cfg.max_buffered
90 };
91 let (stop_tx, _) = watch::channel(false);
92 let (connection_tx, _) = watch::channel(None);
93 Self {
94 inner: Arc::new(Inner {
95 client: cfg.client,
96 channel: cfg.channel,
97 decode: cfg.decode,
98 fetch_snapshot: cfg.fetch_snapshot,
99 read_publication: cfg.read_publication,
100 apply_snapshot: cfg.apply_snapshot,
101 apply_live_publications: cfg.apply_live_publications,
102 max_buffered,
103 on_reconnect: cfg.on_reconnect,
104 on_snapshot_refresh: cfg.on_snapshot_refresh,
105 on_error: Mutex::new(cfg.on_error),
106 ready: AtomicBool::new(false),
107 disposed: AtomicBool::new(false),
108 generation: AtomicU64::new(0),
109 publications: Mutex::new(PublicationState::Buffering(Vec::new())),
110 refresh_gate: AsyncMutex::new(()),
111 refresh_worker_running: AtomicBool::new(false),
112 refresh_requested: AtomicBool::new(false),
113 last_error: Mutex::new(None),
114 stop_tx,
115 connection_tx,
116 started: AtomicBool::new(false),
117 connected_once: AtomicBool::new(false),
118 handles: AtomicUsize::new(1),
119 }),
120 counts_handle: true,
121 }
122 }
123
124 pub async fn start(&self) -> Result<()> {
126 let timeout = self.inner.client.request_timeout();
127 match tokio::time::timeout(timeout, self.start_within_deadline()).await {
128 Ok(result) => result,
129 Err(_) => {
130 let err = Error::realtime(format!(
131 "snapshot-then-stream startup timed out after {timeout:?}"
132 ));
133 self.inner.fail_closed(err.clone());
134 Err(err)
135 }
136 }
137 }
138
139 async fn start_within_deadline(&self) -> Result<()> {
140 if !self.inner.started.swap(true, Ordering::SeqCst) {
141 let inner = self.inner.clone();
142 tokio::spawn(async move {
143 inner.run().await;
144 });
145 }
146 let mut connection_rx = self.inner.connection_tx.subscribe();
147 loop {
148 if self.inner.disposed.load(Ordering::SeqCst) {
149 return match self.err() {
150 Some(err) => Err(err),
151 None => Ok(()),
152 };
153 }
154 if self.inner.connected_once.load(Ordering::SeqCst) {
158 break;
159 }
160 let status = connection_rx.borrow().clone();
161 if let Some(result) = status {
162 result?;
163 break;
164 }
165 if connection_rx.changed().await.is_err() {
166 return Ok(());
167 }
168 }
169 self.refresh_snapshot().await
170 }
171
172 pub async fn refresh_snapshot(&self) -> Result<()> {
178 let _refresh_guard = self.inner.refresh_gate.lock().await;
179 self.inner.refresh_snapshot_once().await
180 }
181
182 pub fn request_refresh(&self) {
188 self.inner.request_refresh();
189 }
190
191 pub fn is_ready(&self) -> bool {
192 self.inner.ready.load(Ordering::SeqCst)
193 }
194
195 pub fn is_disposed(&self) -> bool {
196 self.inner.disposed.load(Ordering::SeqCst)
197 }
198
199 pub fn err(&self) -> Option<Error> {
201 lock_unpoisoned(&self.inner.last_error).clone()
202 }
203
204 pub fn set_on_error<F>(&self, callback: F)
210 where
211 F: Fn(Error) + Send + Sync + 'static,
212 {
213 let callback: SnapshotErrorFn = Arc::new(callback);
214 let current = {
215 *lock_unpoisoned(&self.inner.on_error) = Some(callback.clone());
216 lock_unpoisoned(&self.inner.last_error).clone()
217 };
218 if let Some(err) = current {
219 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback(err)));
220 }
221 }
222
223 pub fn close(&self) {
225 if self.inner.disposed.swap(true, Ordering::SeqCst) {
226 return;
227 }
228 self.inner.generation.fetch_add(1, Ordering::SeqCst);
229 {
230 let mut publications = lock_unpoisoned(&self.inner.publications);
231 *publications = PublicationState::Buffering(Vec::new());
232 }
233 self.inner.ready.store(false, Ordering::SeqCst);
234 self.inner.connection_tx.send_replace(Some(Ok(())));
235 let _ = self.inner.stop_tx.send(true);
236 }
237
238 pub(crate) fn fail(&self, err: Error) {
240 self.inner.fail_closed(err);
241 }
242}
243
244impl<TSnapshot, TPublication> Inner<TSnapshot, TPublication>
245where
246 TSnapshot: Send + 'static,
247 TPublication: Send + 'static,
248{
249 async fn refresh_snapshot_once(&self) -> Result<()> {
250 if self.disposed.load(Ordering::SeqCst) {
251 return match lock_unpoisoned(&self.last_error).clone() {
252 Some(err) => Err(err),
253 None => Ok(()),
254 };
255 }
256 let generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1;
257 self.begin_buffering();
258 let mut stop_rx = self.stop_tx.subscribe();
262 let snapshot = match tokio::select! {
263 _ = stop_rx.changed() => return Ok(()),
264 snapshot = (self.fetch_snapshot)() => snapshot,
265 } {
266 Ok(snapshot) => snapshot,
267 Err(err) => {
268 self.record_error(err.clone());
269 return Err(err);
270 }
271 };
272
273 if self.disposed.load(Ordering::SeqCst)
274 || self.generation.load(Ordering::SeqCst) != generation
275 {
276 return Ok(());
277 }
278 let Some(buffered) = self.take_buffered_if_current(generation) else {
279 return Ok(());
280 };
281 if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
282 (self.apply_snapshot)(snapshot, buffered)
283 }))
284 .is_err()
285 {
286 let err = Error::realtime("apply_snapshot callback panicked".to_owned());
287 self.fail_closed(err.clone());
288 return Err(err);
289 }
290
291 loop {
295 let Some(buffered) = self.take_or_mark_ready(generation) else {
296 return Ok(());
297 };
298 if buffered.is_empty() {
299 break;
300 }
301 if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
302 (self.apply_live_publications)(buffered)
303 }))
304 .is_err()
305 {
306 let err = Error::realtime("apply_live_publications callback panicked".to_owned());
307 self.fail_closed(err.clone());
308 return Err(err);
309 }
310 }
311
312 self.clear_error();
313 if let Some(cb) = &self.on_snapshot_refresh {
314 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| cb()));
315 }
316 Ok(())
317 }
318
319 fn request_refresh(self: &Arc<Self>) {
320 if self.disposed.load(Ordering::SeqCst) {
321 return;
322 }
323 self.refresh_requested.store(true, Ordering::SeqCst);
324 self.generation.fetch_add(1, Ordering::SeqCst);
325 self.begin_buffering();
326 if self
327 .refresh_worker_running
328 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
329 .is_err()
330 {
331 return;
332 }
333 let inner = self.clone();
334 tokio::spawn(async move {
335 inner.run_requested_refreshes().await;
336 });
337 }
338
339 async fn run_requested_refreshes(self: Arc<Self>) {
340 let mut attempts = 0usize;
341 loop {
342 self.refresh_requested.store(false, Ordering::SeqCst);
343 attempts += 1;
344 let last_error = {
345 let _refresh_guard = self.refresh_gate.lock().await;
346 self.refresh_snapshot_once().await
347 }
348 .err();
349
350 if self.disposed.load(Ordering::SeqCst) {
351 return;
352 }
353 let follow_up = self.refresh_requested.load(Ordering::SeqCst);
354 if last_error.is_none() && !follow_up {
355 self.refresh_worker_running.store(false, Ordering::SeqCst);
356 if !self.refresh_requested.load(Ordering::SeqCst)
359 || self
360 .refresh_worker_running
361 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
362 .is_err()
363 {
364 return;
365 }
366 attempts = 0;
367 continue;
368 }
369 if attempts >= MAX_REQUEST_REFRESH_ATTEMPTS {
370 if last_error.is_some() {
371 self.stop_after_recorded_error();
372 } else {
373 self.fail_closed(Error::realtime(
374 "snapshot refresh did not converge after repeated publication gaps"
375 .to_owned(),
376 ));
377 }
378 return;
379 }
380
381 let multiplier = 1u32 << (attempts - 1).min(16);
382 let delay = REQUEST_REFRESH_INITIAL_BACKOFF
383 .saturating_mul(multiplier)
384 .min(REQUEST_REFRESH_MAX_BACKOFF);
385 let mut stop_rx = self.stop_tx.subscribe();
386 let stopped = tokio::select! {
387 changed = stop_rx.changed() => changed.is_err() || *stop_rx.borrow(),
388 _ = tokio::time::sleep(delay) => false,
389 };
390 if stopped {
391 return;
392 }
393 }
394 }
395
396 fn begin_buffering(&self) {
397 let mut publications = lock_unpoisoned(&self.publications);
398 if matches!(*publications, PublicationState::Ready) {
399 *publications = PublicationState::Buffering(Vec::new());
400 }
401 self.ready.store(false, Ordering::SeqCst);
402 }
403
404 fn take_buffered_if_current(&self, generation: u64) -> Option<Vec<TPublication>> {
405 let mut publications = lock_unpoisoned(&self.publications);
406 if self.disposed.load(Ordering::SeqCst)
407 || self.generation.load(Ordering::SeqCst) != generation
408 {
409 return None;
410 }
411 match &mut *publications {
412 PublicationState::Buffering(pending) => Some(std::mem::take(pending)),
413 PublicationState::Ready => None,
414 }
415 }
416
417 fn take_or_mark_ready(&self, generation: u64) -> Option<Vec<TPublication>> {
418 let mut publications = lock_unpoisoned(&self.publications);
419 if self.disposed.load(Ordering::SeqCst)
420 || self.generation.load(Ordering::SeqCst) != generation
421 {
422 return None;
423 }
424 match &mut *publications {
425 PublicationState::Buffering(pending) if pending.is_empty() => {
426 *publications = PublicationState::Ready;
427 self.ready.store(true, Ordering::SeqCst);
428 Some(Vec::new())
429 }
430 PublicationState::Buffering(pending) => Some(std::mem::take(pending)),
431 PublicationState::Ready => Some(Vec::new()),
432 }
433 }
434
435 fn clear_publications(&self) {
436 let mut publications = lock_unpoisoned(&self.publications);
437 *publications = PublicationState::Buffering(Vec::new());
438 }
439}
440
441impl<TSnapshot, TPublication> Drop for SnapshotThenStream<TSnapshot, TPublication> {
442 fn drop(&mut self) {
443 if !self.counts_handle {
444 return;
445 }
446 if self.inner.handles.fetch_sub(1, Ordering::SeqCst) != 1 {
449 return;
450 }
451 if self.inner.disposed.swap(true, Ordering::SeqCst) {
452 return;
453 }
454 self.inner.generation.fetch_add(1, Ordering::SeqCst);
455 {
456 let mut publications = lock_unpoisoned(&self.inner.publications);
457 *publications = PublicationState::Buffering(Vec::new());
458 }
459 self.inner.ready.store(false, Ordering::SeqCst);
460 self.inner.connection_tx.send_replace(Some(Ok(())));
461 let _ = self.inner.stop_tx.send(true);
462 }
463}
464
465impl<TSnapshot, TPublication> Clone for SnapshotThenStream<TSnapshot, TPublication> {
466 fn clone(&self) -> Self {
467 self.inner.handles.fetch_add(1, Ordering::SeqCst);
468 Self {
469 inner: self.inner.clone(),
470 counts_handle: true,
471 }
472 }
473}
474
475impl<TSnapshot, TPublication> Inner<TSnapshot, TPublication>
476where
477 TSnapshot: Send + 'static,
478 TPublication: Send + 'static,
479{
480 fn fail_closed(&self, err: Error) {
481 self.ready.store(false, Ordering::SeqCst);
482 self.disposed.store(true, Ordering::SeqCst);
483 self.generation.fetch_add(1, Ordering::SeqCst);
484 self.clear_publications();
485 self.record_error(err.clone());
486 self.connection_tx.send_replace(Some(Err(err)));
487 let _ = self.stop_tx.send(true);
488 }
489
490 fn stop_after_recorded_error(&self) {
491 self.ready.store(false, Ordering::SeqCst);
492 self.disposed.store(true, Ordering::SeqCst);
493 self.generation.fetch_add(1, Ordering::SeqCst);
494 self.clear_publications();
495 let _ = self.stop_tx.send(true);
496 }
497
498 async fn run(self: Arc<Self>) {
499 let mut stop_rx = self.stop_tx.subscribe();
500 let mut first = true;
501 let mut backoff = ReconnectBackoff::new();
502 loop {
503 if self.disposed.load(Ordering::SeqCst) || *stop_rx.borrow() {
504 break;
505 }
506 let decode = self.decode.clone();
507 let sub = tokio::select! {
508 changed = stop_rx.changed() => {
509 if changed.is_err() || *stop_rx.borrow() {
510 break;
511 }
512 continue;
513 }
514 sub = self.client.subscribe_proto_with_options(
515 &self.channel,
516 move |bytes| decode(bytes),
517 false,
518 ) => {
519 match sub {
520 Ok(sub) => {
521 backoff.reset();
522 self.connected_once.store(true, Ordering::SeqCst);
523 self.connection_tx.send_replace(Some(Ok(())));
524 sub
525 }
526 Err(err) => {
527 self.connection_tx.send_replace(Some(Err(err)));
528 if self.disposed.load(Ordering::SeqCst) {
529 break;
530 }
531 let delay = backoff.next_delay();
532 let stopped = tokio::select! {
533 changed = stop_rx.changed() => {
534 changed.is_err() || *stop_rx.borrow()
535 }
536 _ = tokio::time::sleep(delay) => false,
537 };
538 if stopped {
539 break;
540 }
541 self.connection_tx.send_replace(None);
542 continue;
543 }
544 }
545 }
546 };
547 if self.disposed.load(Ordering::SeqCst) || *stop_rx.borrow() {
548 sub.close();
549 break;
550 }
551 if !first {
552 if let Some(cb) = &self.on_reconnect {
553 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| cb()));
554 }
555 let this = SnapshotThenStream {
556 inner: self.clone(),
557 counts_handle: false,
558 };
559 let mut refresh = this.refresh_snapshot().await;
561 if refresh.is_err() {
562 refresh = this.refresh_snapshot().await;
563 }
564 if refresh.is_err() {
565 self.stop_after_recorded_error();
569 self.ready.store(false, Ordering::SeqCst);
570 sub.close();
571 break;
572 }
573 }
574 first = false;
575 if let Err(err) = self.pump_subscription(sub, &mut stop_rx).await
576 && !self.disposed.load(Ordering::SeqCst)
577 {
578 self.record_error(err);
579 }
580 self.connection_tx.send_replace(None);
581 if self.disposed.load(Ordering::SeqCst) || *stop_rx.borrow() {
582 break;
583 }
584 let delay = backoff.next_delay();
585 let stopped = tokio::select! {
586 changed = stop_rx.changed() => changed.is_err() || *stop_rx.borrow(),
587 _ = tokio::time::sleep(delay) => false,
588 };
589 if stopped {
590 break;
591 }
592 }
593 }
594
595 async fn pump_subscription(
596 &self,
597 mut sub: TypedSubscription<TPublication>,
598 stop_rx: &mut watch::Receiver<bool>,
599 ) -> Result<()> {
600 loop {
601 tokio::select! {
602 changed = stop_rx.changed() => {
603 if changed.is_err() || *stop_rx.borrow() {
604 sub.close();
605 return Ok(());
606 }
607 }
608 item = sub.recv_result() => {
609 match item {
610 Ok(Some(msg)) => self.handle_publication(msg)?,
611 Ok(None) => return Ok(()),
612 Err(err) => return Err(err),
613 }
614 }
615 }
616 }
617 }
618
619 fn handle_publication(&self, msg: TPublication) -> Result<()> {
620 if self.disposed.load(Ordering::SeqCst) {
621 return Ok(());
622 }
623 let items = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
624 (self.read_publication)(msg)
625 })) {
626 Ok(items) => items,
627 Err(_) => {
628 let err = Error::realtime("read_publication callback panicked".to_owned());
629 self.fail_closed(err.clone());
630 return Err(err);
631 }
632 };
633 if items.is_empty() {
634 return Ok(());
635 }
636 let live_items = {
637 let mut publications = lock_unpoisoned(&self.publications);
638 match &mut *publications {
639 PublicationState::Buffering(pending) => {
640 pending.extend(items);
641 if pending.len() > self.max_buffered {
642 pending.clear();
643 None
644 } else {
645 return Ok(());
646 }
647 }
648 PublicationState::Ready => Some(items),
649 }
650 };
651 let Some(items) = live_items else {
652 self.fail_closed(Error::queue_overflow(
653 "snapshot recovery buffer full; recreate the subscription",
654 ));
655 return Ok(());
656 };
657 if self.disposed.load(Ordering::SeqCst) {
658 return Ok(());
659 }
660 if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
661 (self.apply_live_publications)(items)
662 }))
663 .is_err()
664 {
665 let err = Error::realtime("apply_live_publications callback panicked".to_owned());
666 self.fail_closed(err.clone());
667 return Err(err);
668 }
669 Ok(())
670 }
671
672 fn record_error(&self, err: Error) {
673 let callback = {
674 *lock_unpoisoned(&self.last_error) = Some(err.clone());
675 lock_unpoisoned(&self.on_error).clone()
676 };
677 if let Some(callback) = callback {
678 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback(err)));
679 }
680 }
681
682 fn clear_error(&self) {
683 *lock_unpoisoned(&self.last_error) = None;
684 }
685}
686
687#[cfg(test)]
688mod tests {
689 use super::*;
690 use futures_util::future::FutureExt;
691 use std::sync::Barrier;
692 use std::sync::atomic::AtomicUsize;
693
694 #[tokio::test]
695 async fn refresh_snapshot_fires_on_snapshot_refresh() {
696 let fired = Arc::new(AtomicUsize::new(0));
697 let fired_cb = fired.clone();
698 let client = Client::new(
699 "wss://example.invalid",
700 "https://example.invalid",
701 None,
702 None,
703 );
704 let sts = SnapshotThenStream::new(SnapshotThenStreamConfig {
705 client,
706 channel: "public:test".into(),
707 decode: Arc::new(|_b| Ok(1u8)),
708 fetch_snapshot: Arc::new(|| async { Ok("snap".to_string()) }.boxed()),
709 read_publication: Arc::new(|p| vec![p]),
710 apply_snapshot: Arc::new(|_s, _p| {}),
711 apply_live_publications: Arc::new(|_p| {}),
712 max_buffered: 8,
713 on_reconnect: None,
714 on_snapshot_refresh: Some(Arc::new(move || {
715 fired_cb.fetch_add(1, Ordering::SeqCst);
716 })),
717 on_error: None,
718 });
719 sts.refresh_snapshot().await.expect("refresh");
720 assert_eq!(fired.load(Ordering::SeqCst), 1);
721 assert!(sts.is_ready());
722 }
723
724 #[tokio::test]
725 async fn failed_refresh_retains_buffer_and_success_merges_each_item_once() {
726 let attempts = Arc::new(AtomicUsize::new(0));
727 let fetch_attempts = attempts.clone();
728 let merged = Arc::new(Mutex::new(Vec::<u8>::new()));
729 let merged_cb = merged.clone();
730 let client = Client::new(
731 "wss://example.invalid",
732 "https://example.invalid",
733 None,
734 None,
735 );
736 let sts = SnapshotThenStream::new(SnapshotThenStreamConfig {
737 client,
738 channel: "public:test".into(),
739 decode: Arc::new(|_b| Ok(1u8)),
740 fetch_snapshot: Arc::new(move || {
741 let attempt = fetch_attempts.fetch_add(1, Ordering::SeqCst);
742 async move {
743 if attempt == 0 {
744 Err(Error::transport("snapshot refresh failed"))
745 } else {
746 Ok("recovered".to_owned())
747 }
748 }
749 .boxed()
750 }),
751 read_publication: Arc::new(|p| vec![p]),
752 apply_snapshot: Arc::new(move |_snapshot, pending| {
753 merged_cb.lock().expect("merged lock").extend(pending);
754 }),
755 apply_live_publications: Arc::new(|_publications| {}),
756 max_buffered: 8,
757 on_reconnect: None,
758 on_snapshot_refresh: None,
759 on_error: None,
760 });
761
762 assert!(sts.refresh_snapshot().await.is_err());
763 assert!(!sts.is_ready());
764 assert!(sts.err().is_some());
765
766 sts.inner.handle_publication(7).expect("buffer 7");
767 sts.inner.handle_publication(9).expect("buffer 9");
768 {
769 let publications = lock_unpoisoned(&sts.inner.publications);
770 let PublicationState::Buffering(pending) = &*publications else {
771 panic!("expected buffering state");
772 };
773 assert_eq!(pending.as_slice(), &[7, 9]);
774 }
775
776 sts.refresh_snapshot().await.expect("retry succeeds");
777 assert!(sts.is_ready());
778 assert!(sts.err().is_none());
779 assert_eq!(merged.lock().expect("merged lock").as_slice(), &[7, 9]);
780 assert!(matches!(
781 *lock_unpoisoned(&sts.inner.publications),
782 PublicationState::Ready
783 ));
784
785 sts.refresh_snapshot()
786 .await
787 .expect("later refresh succeeds");
788 assert_eq!(
789 merged.lock().expect("merged lock").as_slice(),
790 &[7, 9],
791 "buffered publications must be applied exactly once"
792 );
793 }
794
795 #[tokio::test]
796 async fn refresh_snapshot_retries_after_initial_failure() {
797 let attempts = Arc::new(AtomicUsize::new(0));
798 let fetch_attempts = attempts.clone();
799 let client = Client::new(
800 "wss://example.invalid",
801 "https://example.invalid",
802 None,
803 None,
804 );
805 let sts = SnapshotThenStream::new(SnapshotThenStreamConfig {
806 client,
807 channel: "public:test".into(),
808 decode: Arc::new(|_b| Ok(1u8)),
809 fetch_snapshot: Arc::new(move || {
810 let attempt = fetch_attempts.fetch_add(1, Ordering::SeqCst);
811 async move {
812 if attempt == 0 {
813 Err(crate::Error::transport("transient snapshot failure"))
814 } else {
815 Ok("snap".to_string())
816 }
817 }
818 .boxed()
819 }),
820 read_publication: Arc::new(|p| vec![p]),
821 apply_snapshot: Arc::new(|_s, _p| {}),
822 apply_live_publications: Arc::new(|_p| {}),
823 max_buffered: 8,
824 on_reconnect: None,
825 on_snapshot_refresh: None,
826 on_error: None,
827 });
828
829 assert!(sts.refresh_snapshot().await.is_err());
830 assert!(sts.err().is_some());
831 assert!(!sts.is_ready());
832 sts.refresh_snapshot().await.expect("snapshot retry");
833 assert_eq!(attempts.load(Ordering::SeqCst), 2);
834 assert!(sts.is_ready());
835 assert!(sts.err().is_none());
836 sts.close();
837 }
838
839 #[tokio::test]
840 async fn refresh_snapshot_failure_retains_buffer_for_successful_retry() {
841 let attempts = Arc::new(AtomicUsize::new(0));
842 let fetch_attempts = attempts.clone();
843 let merged = Arc::new(Mutex::new(Vec::<u8>::new()));
844 let merged_cb = merged.clone();
845 let client = Client::new(
846 "wss://example.invalid",
847 "https://example.invalid",
848 None,
849 None,
850 );
851 let sts = SnapshotThenStream::new(SnapshotThenStreamConfig {
852 client,
853 channel: "public:test".into(),
854 decode: Arc::new(|_b| Ok(1u8)),
855 fetch_snapshot: Arc::new(move || {
856 let attempt = fetch_attempts.fetch_add(1, Ordering::SeqCst);
857 async move {
858 if attempt == 0 {
859 Err(crate::Error::transport("transient snapshot failure"))
860 } else {
861 Ok("snap".to_string())
862 }
863 }
864 .boxed()
865 }),
866 read_publication: Arc::new(|p| vec![p]),
867 apply_snapshot: Arc::new(move |_s, pending| {
868 merged_cb.lock().expect("merged").extend(pending);
869 }),
870 apply_live_publications: Arc::new(|_p| {}),
871 max_buffered: 8,
872 on_reconnect: None,
873 on_snapshot_refresh: None,
874 on_error: None,
875 });
876
877 sts.inner.handle_publication(10).expect("buffer 10");
879 sts.inner.handle_publication(11).expect("buffer 11");
880 assert!(sts.refresh_snapshot().await.is_err());
881 sts.inner.handle_publication(12).expect("buffer 12");
882 sts.refresh_snapshot().await.expect("retry");
883 let got = merged.lock().expect("merged").clone();
884 assert_eq!(
885 got,
886 vec![10, 11, 12],
887 "each buffered pub merged exactly once"
888 );
889 assert!(sts.err().is_none());
890 sts.close();
891 }
892
893 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
894 async fn publication_arriving_after_pending_take_is_drained_exactly_once() {
895 let snapshot_entered = Arc::new(Barrier::new(2));
896 let release_snapshot = Arc::new(Barrier::new(2));
897 let snapshot_calls = Arc::new(AtomicUsize::new(0));
898 let live = Arc::new(Mutex::new(Vec::<u8>::new()));
899 let client = Client::new(
900 "wss://example.invalid",
901 "https://example.invalid",
902 None,
903 None,
904 );
905 let sts = SnapshotThenStream::new(SnapshotThenStreamConfig {
906 client,
907 channel: "public:test".into(),
908 decode: Arc::new(|_b| Ok(1u8)),
909 fetch_snapshot: Arc::new(|| async { Ok("snap".to_owned()) }.boxed()),
910 read_publication: Arc::new(|p| vec![p]),
911 apply_snapshot: {
912 let entered = snapshot_entered.clone();
913 let release = release_snapshot.clone();
914 let calls = snapshot_calls.clone();
915 Arc::new(move |_snapshot, pending| {
916 assert!(pending.is_empty());
917 if calls.fetch_add(1, Ordering::SeqCst) == 0 {
918 entered.wait();
919 release.wait();
920 }
921 })
922 },
923 apply_live_publications: {
924 let live = live.clone();
925 Arc::new(move |publications| {
926 live.lock().expect("live lock").extend(publications);
927 })
928 },
929 max_buffered: 8,
930 on_reconnect: None,
931 on_snapshot_refresh: None,
932 on_error: None,
933 });
934
935 let refresh = {
936 let sts = sts.clone();
937 tokio::spawn(async move { sts.refresh_snapshot().await })
938 };
939 snapshot_entered.wait();
940 sts.inner
941 .handle_publication(42)
942 .expect("publication in vulnerable window");
943 release_snapshot.wait();
944 refresh.await.expect("refresh task").expect("refresh");
945
946 assert!(sts.is_ready());
947 assert_eq!(live.lock().expect("live lock").as_slice(), &[42]);
948 sts.refresh_snapshot().await.expect("second refresh");
949 assert_eq!(
950 live.lock().expect("live lock").as_slice(),
951 &[42],
952 "drained publication must not be replayed"
953 );
954 }
955
956 #[tokio::test]
957 async fn request_refresh_burst_is_single_flight_and_coalesced() {
958 let attempts = Arc::new(AtomicUsize::new(0));
959 let active = Arc::new(AtomicUsize::new(0));
960 let max_active = Arc::new(AtomicUsize::new(0));
961 let first_started = Arc::new(tokio::sync::Semaphore::new(0));
962 let release_first = Arc::new(tokio::sync::Semaphore::new(0));
963 let client = Client::new(
964 "wss://example.invalid",
965 "https://example.invalid",
966 None,
967 None,
968 );
969 let sts = SnapshotThenStream::new(SnapshotThenStreamConfig {
970 client,
971 channel: "public:test".into(),
972 decode: Arc::new(|_b| Ok(1u8)),
973 fetch_snapshot: {
974 let attempts = attempts.clone();
975 let active = active.clone();
976 let max_active = max_active.clone();
977 let first_started = first_started.clone();
978 let release_first = release_first.clone();
979 Arc::new(move || {
980 let attempt = attempts.fetch_add(1, Ordering::SeqCst);
981 let now_active = active.fetch_add(1, Ordering::SeqCst) + 1;
982 max_active.fetch_max(now_active, Ordering::SeqCst);
983 let active = active.clone();
984 let first_started = first_started.clone();
985 let release_first = release_first.clone();
986 async move {
987 if attempt == 0 {
988 first_started.add_permits(1);
989 release_first.acquire().await.expect("release").forget();
990 }
991 active.fetch_sub(1, Ordering::SeqCst);
992 Ok("snap".to_owned())
993 }
994 .boxed()
995 })
996 },
997 read_publication: Arc::new(|p| vec![p]),
998 apply_snapshot: Arc::new(|_snapshot: String, _pending: Vec<u8>| {}),
999 apply_live_publications: Arc::new(|_publications| {}),
1000 max_buffered: 8,
1001 on_reconnect: None,
1002 on_snapshot_refresh: None,
1003 on_error: None,
1004 });
1005
1006 sts.request_refresh();
1007 first_started.acquire().await.expect("first fetch").forget();
1008 for _ in 0..100 {
1009 sts.request_refresh();
1010 }
1011 release_first.add_permits(1);
1012 tokio::time::timeout(Duration::from_secs(2), async {
1013 while sts.inner.refresh_worker_running.load(Ordering::SeqCst) {
1014 tokio::task::yield_now().await;
1015 }
1016 })
1017 .await
1018 .expect("refresh worker completion");
1019
1020 assert_eq!(max_active.load(Ordering::SeqCst), 1);
1021 assert_eq!(
1022 attempts.load(Ordering::SeqCst),
1023 2,
1024 "the burst should produce one in-flight fetch and one coalesced follow-up"
1025 );
1026 assert!(sts.is_ready());
1027 }
1028
1029 #[tokio::test]
1030 async fn request_refresh_persistent_failure_retries_bounded_then_fails_closed() {
1031 let attempts = Arc::new(AtomicUsize::new(0));
1032 let active = Arc::new(AtomicUsize::new(0));
1033 let max_active = Arc::new(AtomicUsize::new(0));
1034 let client = Client::new(
1035 "wss://example.invalid",
1036 "https://example.invalid",
1037 None,
1038 None,
1039 );
1040 let sts = SnapshotThenStream::new(SnapshotThenStreamConfig {
1041 client,
1042 channel: "public:test".into(),
1043 decode: Arc::new(|_b| Ok(1u8)),
1044 fetch_snapshot: {
1045 let attempts = attempts.clone();
1046 let active = active.clone();
1047 let max_active = max_active.clone();
1048 Arc::new(move || {
1049 attempts.fetch_add(1, Ordering::SeqCst);
1050 let now_active = active.fetch_add(1, Ordering::SeqCst) + 1;
1051 max_active.fetch_max(now_active, Ordering::SeqCst);
1052 let active = active.clone();
1053 async move {
1054 active.fetch_sub(1, Ordering::SeqCst);
1055 Err(Error::transport("persistent snapshot failure"))
1056 }
1057 .boxed()
1058 })
1059 },
1060 read_publication: Arc::new(|p| vec![p]),
1061 apply_snapshot: Arc::new(|_snapshot: String, _pending: Vec<u8>| {}),
1062 apply_live_publications: Arc::new(|_publications| {}),
1063 max_buffered: 8,
1064 on_reconnect: None,
1065 on_snapshot_refresh: None,
1066 on_error: None,
1067 });
1068
1069 sts.request_refresh();
1070 tokio::time::timeout(Duration::from_secs(2), async {
1071 while !sts.is_disposed() {
1072 tokio::task::yield_now().await;
1073 }
1074 })
1075 .await
1076 .expect("bounded failure completion");
1077
1078 assert_eq!(
1079 attempts.load(Ordering::SeqCst),
1080 MAX_REQUEST_REFRESH_ATTEMPTS
1081 );
1082 assert_eq!(max_active.load(Ordering::SeqCst), 1);
1083 assert!(!sts.is_ready());
1084 assert!(matches!(sts.err(), Some(Error::Transport(_))));
1085 }
1086
1087 #[tokio::test]
1088 async fn request_refresh_persistent_gaps_fail_closed_after_bounded_followups() {
1089 let attempts = Arc::new(AtomicUsize::new(0));
1090 let stream_slot: Arc<Mutex<Option<SnapshotThenStream<String, u8>>>> =
1091 Arc::new(Mutex::new(None));
1092 let client = Client::new(
1093 "wss://example.invalid",
1094 "https://example.invalid",
1095 None,
1096 None,
1097 );
1098 let sts = SnapshotThenStream::new(SnapshotThenStreamConfig {
1099 client,
1100 channel: "public:test".into(),
1101 decode: Arc::new(|_b| Ok(1u8)),
1102 fetch_snapshot: {
1103 let attempts = attempts.clone();
1104 Arc::new(move || {
1105 attempts.fetch_add(1, Ordering::SeqCst);
1106 async { Ok("snap".to_owned()) }.boxed()
1107 })
1108 },
1109 read_publication: Arc::new(|p| vec![p]),
1110 apply_snapshot: {
1111 let stream_slot = stream_slot.clone();
1112 Arc::new(move |_snapshot, _pending| {
1113 lock_unpoisoned(&stream_slot)
1114 .as_ref()
1115 .expect("stream installed")
1116 .request_refresh();
1117 })
1118 },
1119 apply_live_publications: Arc::new(|_publications| {}),
1120 max_buffered: 8,
1121 on_reconnect: None,
1122 on_snapshot_refresh: None,
1123 on_error: None,
1124 });
1125 *lock_unpoisoned(&stream_slot) = Some(sts.clone());
1126
1127 sts.request_refresh();
1128 tokio::time::timeout(Duration::from_secs(2), async {
1129 while !sts.is_disposed() {
1130 tokio::task::yield_now().await;
1131 }
1132 })
1133 .await
1134 .expect("bounded persistent-gap completion");
1135
1136 assert_eq!(
1137 attempts.load(Ordering::SeqCst),
1138 MAX_REQUEST_REFRESH_ATTEMPTS
1139 );
1140 assert!(!sts.is_ready());
1141 assert!(matches!(sts.err(), Some(Error::Realtime(_))));
1142 lock_unpoisoned(&stream_slot).take();
1143 }
1144
1145 #[test]
1146 fn snapshot_buffer_overflow_fails_closed() {
1147 let client = Client::new(
1148 "wss://example.invalid",
1149 "https://example.invalid",
1150 None,
1151 None,
1152 );
1153 let sts = SnapshotThenStream::new(SnapshotThenStreamConfig {
1154 client,
1155 channel: "public:test".into(),
1156 decode: Arc::new(|_b| Ok(1u8)),
1157 fetch_snapshot: Arc::new(|| async { Ok("snap".to_string()) }.boxed()),
1158 read_publication: Arc::new(|p| vec![p]),
1159 apply_snapshot: Arc::new(|_s, _p| {}),
1160 apply_live_publications: Arc::new(|_p| {}),
1161 max_buffered: 1,
1162 on_reconnect: None,
1163 on_snapshot_refresh: None,
1164 on_error: None,
1165 });
1166
1167 sts.inner.handle_publication(1).expect("first publication");
1168 assert!(!sts.is_disposed());
1169 sts.inner
1170 .handle_publication(2)
1171 .expect("overflow is persisted on the coordinator");
1172 assert!(sts.is_disposed());
1173 assert!(matches!(sts.err(), Some(Error::QueueOverflow(_))));
1174 }
1175
1176 #[test]
1177 fn dropping_last_public_handle_disposes_background_coordinator() {
1178 let client = Client::new(
1179 "wss://example.invalid",
1180 "https://example.invalid",
1181 None,
1182 None,
1183 );
1184 let sts = SnapshotThenStream::new(SnapshotThenStreamConfig {
1185 client,
1186 channel: "public:test".into(),
1187 decode: Arc::new(|_b| Ok(1u8)),
1188 fetch_snapshot: Arc::new(|| async { Ok("snap".to_string()) }.boxed()),
1189 read_publication: Arc::new(|p| vec![p]),
1190 apply_snapshot: Arc::new(|_s, _p| {}),
1191 apply_live_publications: Arc::new(|_p| {}),
1192 max_buffered: 1,
1193 on_reconnect: None,
1194 on_snapshot_refresh: None,
1195 on_error: None,
1196 });
1197 let clone = sts.clone();
1198 let observer = sts.inner.clone();
1199 drop(sts);
1200 assert!(
1201 !observer.disposed.load(Ordering::SeqCst),
1202 "one public clone remains"
1203 );
1204 drop(clone);
1205 assert!(
1206 observer.disposed.load(Ordering::SeqCst),
1207 "last public handle must stop the coordinator even when tasks hold Arc references"
1208 );
1209 }
1210}