arminius_lib_nats/
resubscribe.rs1use std::{
8 ops::ControlFlow,
9 pin::Pin,
10 sync::{
11 Arc,
12 atomic::{AtomicBool, Ordering},
13 },
14 time::Duration,
15};
16
17use arminius_lib_message_bus::Error;
18use backon::{BackoffBuilder as _, ExponentialBuilder};
19use futures::{Stream, StreamExt as _};
20
21const RECONNECT_MAX_DELAY: Duration = Duration::from_secs(32);
24
25pub type MessageStream<T> = Pin<Box<dyn Stream<Item = T> + Send>>;
28
29#[derive(Debug)]
32pub struct ConnectionSignals {
33 pub connected: Arc<AtomicBool>,
36 pub reconnect_notify: Arc<tokio::sync::Notify>,
39}
40
41pub enum ForwardOutcome {
43 Resubscribe,
45 ReceiverDropped,
48}
49
50pub async fn forward_stream<T: Send + 'static>(
57 signals: &ConnectionSignals,
58 subject: &'static str,
59 mut stream: MessageStream<T>,
60 tx: &tokio::sync::mpsc::Sender<T>,
61) -> ForwardOutcome {
62 while let Some(item) = stream.next().await {
63 if !signals.connected.load(Ordering::SeqCst) {
64 tracing::warn!(subject, "Detected disconnection, will re-subscribe after reconnection");
65 return ForwardOutcome::Resubscribe;
66 }
67 match tx.try_send(item) {
69 Ok(()) => {}
70 Err(tokio::sync::mpsc::error::TrySendError::Full(item)) => {
71 tracing::warn!(subject, "Internal channel full, waiting to send");
73 if let Err(error) = tx.send(item).await {
74 tracing::error!(subject, %error, "Receiver dropped while waiting to send");
75 return ForwardOutcome::ReceiverDropped;
76 }
77 }
78 Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
79 tracing::warn!(subject, "Receiver closed while forwarding");
80 return ForwardOutcome::ReceiverDropped;
81 }
82 }
83 }
84
85 tracing::warn!(subject, "Stream closed, will re-subscribe");
86 ForwardOutcome::Resubscribe
87}
88
89async fn await_connection<T>(
95 signals: &ConnectionSignals,
96 tx: &tokio::sync::mpsc::Sender<T>,
97) -> ControlFlow<()> {
98 if signals.connected.load(Ordering::SeqCst) {
99 return ControlFlow::Continue(());
100 }
101
102 tokio::select! {
103 () = signals.reconnect_notify.notified() => ControlFlow::Continue(()),
104 () = tx.closed() => ControlFlow::Break(()),
105 }
106}
107
108async fn resubscribe<T, F, Fut>(
114 signals: &ConnectionSignals,
115 subject: &'static str,
116 make_stream: &F,
117 tx: &tokio::sync::mpsc::Sender<T>,
118) -> Option<MessageStream<T>>
119where
120 F: Fn() -> Fut + Sync,
121 Fut: Future<Output = Result<MessageStream<T>, Error>>,
122{
123 let mut backoff = ExponentialBuilder::default()
126 .with_max_delay(RECONNECT_MAX_DELAY)
127 .without_max_times()
128 .build();
129
130 loop {
131 if await_connection(signals, tx).await.is_break() {
132 return None;
133 }
134
135 match make_stream().await {
136 Ok(stream) => {
137 tracing::debug!(subject, "Re-subscribed after reconnection");
138 return Some(stream);
139 }
140 Err(error) => {
141 let delay = backoff.next().unwrap_or(RECONNECT_MAX_DELAY);
142 tracing::error!(
143 subject,
144 %error,
145 delay_secs = delay.as_secs(),
146 "Failed to re-subscribe, retrying with backoff"
147 );
148 tokio::time::sleep(delay).await;
149 }
150 }
151 }
152}
153
154async fn forward_and_resubscribe<T, F, Fut>(
158 signals: &ConnectionSignals,
159 subject: &'static str,
160 initial: MessageStream<T>,
161 make_stream: F,
162 tx: tokio::sync::mpsc::Sender<T>,
163) where
164 T: Send + 'static,
165 F: Fn() -> Fut + Sync,
166 Fut: Future<Output = Result<MessageStream<T>, Error>>,
167{
168 let mut stream = initial;
169
170 loop {
171 if matches!(
172 forward_stream(signals, subject, stream, &tx).await,
173 ForwardOutcome::ReceiverDropped
174 ) {
175 break;
176 }
177
178 match resubscribe(signals, subject, &make_stream, &tx).await {
179 Some(next) => stream = next,
180 None => break,
181 }
182 }
183
184 tracing::debug!(subject, "Receiver dropped, stopping subscription task");
185}
186
187pub fn resubscribing_stream<T, F, Fut>(
200 signals: Arc<ConnectionSignals>,
201 channel_size: usize,
202 subject: &'static str,
203 initial: MessageStream<T>,
204 make_stream: F,
205) -> impl Stream<Item = T> + Send + 'static
206where
207 T: Send + 'static,
208 F: Fn() -> Fut + Send + Sync + 'static,
209 Fut: Future<Output = Result<MessageStream<T>, Error>> + Send,
210{
211 let (tx, rx) = tokio::sync::mpsc::channel(channel_size);
212
213 tokio::spawn(async move {
214 forward_and_resubscribe(&signals, subject, initial, make_stream, tx).await;
215 });
216
217 futures::stream::unfold(rx, |mut rx| async move { rx.recv().await.map(|item| (item, rx)) })
218}
219
220#[cfg(test)]
221mod tests;