1use std::future::Future;
23use std::sync::Mutex;
24use std::time::Duration;
25
26use tokio::sync::mpsc;
27use tokio_util::sync::CancellationToken;
28use tonic::{Status, Streaming};
29use tracing::{debug, warn};
30
31use crate::error::map_status;
32
33pub fn backoff(attempt: u32) -> Duration {
36 let ms = 500u64.saturating_mul(1u64 << attempt.min(6));
37 Duration::from_millis(ms.min(30_000))
38}
39
40pub fn fatal_open(status: &Status) -> Option<String> {
59 use brokapi_core::Error;
60 match map_status(status) {
61 Error::Config(e) => Some(format!("unauthenticated: {e}")),
62 Error::Rejected { code, message }
63 if code == "streams_limit" || code == "PermissionDenied" =>
64 {
65 Some(format!("{code}: {message}"))
66 }
67 _ => None,
68 }
69}
70
71#[derive(Default)]
73pub struct LastError(Mutex<Option<String>>);
74
75impl LastError {
76 pub fn set(&self, msg: impl Into<String>) {
78 *self.0.lock().expect("poisoned") = Some(msg.into());
79 }
80
81 pub fn clear(&self) {
83 *self.0.lock().expect("poisoned") = None;
84 }
85
86 pub fn get(&self) -> Option<String> {
101 self.0.lock().expect("poisoned").clone()
102 }
103}
104
105pub async fn sleep_or_cancel(d: Duration, cancel: &CancellationToken) -> bool {
123 tokio::select! {
124 _ = cancel.cancelled() => false,
125 _ = tokio::time::sleep(d) => true,
126 }
127}
128
129#[allow(clippy::too_many_arguments)]
138pub async fn supervise<M, F, Fut, Frame>(
139 name: &'static str,
140 open: F,
141 connected: impl Fn() -> Frame,
142 wrap: impl Fn(M) -> Frame,
143 failed: impl Fn(String) -> Frame,
144 tx: mpsc::Sender<Frame>,
145 stale_after: Duration,
146 last_error: std::sync::Arc<LastError>,
147 cancel: CancellationToken,
148) where
149 F: Fn() -> Fut,
150 Fut: Future<Output = Result<Streaming<M>, Status>>,
151{
152 let mut attempt = 0u32;
153 loop {
154 if cancel.is_cancelled() {
155 return;
156 }
157 let opened = tokio::select! {
158 _ = cancel.cancelled() => return,
159 r = open() => r,
160 };
161 let reason = match opened {
162 Ok(mut stream) => {
163 last_error.clear();
164 debug!(stream = name, "stream open");
165 if tx.send(connected()).await.is_err() {
166 return;
167 }
168
169 loop {
170 let msg = tokio::select! {
171 _ = cancel.cancelled() => return,
172 m = tokio::time::timeout(stale_after, stream.message()) => m,
173 };
174 match msg {
175 Ok(Ok(Some(m))) => {
176 attempt = 0;
178 if tx.send(wrap(m)).await.is_err() {
179 return;
180 }
181 }
182 Ok(Ok(None)) => break "closed by the server".to_string(),
183 Ok(Err(status)) => {
184 break format!("{}: {}", status.code(), status.message());
185 }
186 Err(_) => break format!("silent for {stale_after:?}"),
187 }
188 }
189 }
190 Err(status) => {
191 if let Some(reason) = fatal_open(&status) {
192 warn!(stream = name, %reason, "stream refused for good, not reopened");
193 last_error.set(format!("{name}: {reason}"));
194 let _ = tx.send(failed(reason)).await;
195 return;
196 }
197 format!("open failed: {}: {}", status.code(), status.message())
198 }
199 };
200 warn!(stream = name, %reason, "stream down, reconnecting");
201 last_error.set(format!("{name}: {reason}"));
202 if !sleep_or_cancel(backoff(attempt), &cancel).await {
203 return;
204 }
205 attempt = attempt.saturating_add(1);
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn backoff_grows_and_caps() {
215 assert_eq!(backoff(0), Duration::from_millis(500));
216 assert_eq!(backoff(1), Duration::from_secs(1));
217 assert_eq!(backoff(3), Duration::from_secs(4));
218 assert_eq!(backoff(30), Duration::from_secs(30));
219 }
220
221 use std::sync::Arc;
222 use std::sync::atomic::{AtomicUsize, Ordering};
223
224 #[test]
225 fn only_a_final_refusal_stops_a_stream() {
226 assert!(
228 fatal_open(&Status::unauthenticated("40003"))
229 .unwrap()
230 .starts_with("unauthenticated")
231 );
232 assert!(
233 fatal_open(&Status::resource_exhausted("80001"))
234 .unwrap()
235 .starts_with("streams_limit")
236 );
237 assert!(
238 fatal_open(&Status::permission_denied("no"))
239 .unwrap()
240 .starts_with("PermissionDenied")
241 );
242 for retry in [
244 Status::unavailable("down"),
245 Status::deadline_exceeded("slow"),
246 Status::internal("oops"),
247 Status::resource_exhausted("80002"),
248 Status::resource_exhausted("80005"),
249 Status::not_found("x"),
250 ] {
251 assert!(fatal_open(&retry).is_none(), "{retry:?}");
252 }
253 }
254
255 type Opened = std::future::Ready<Result<Streaming<u8>, Status>>;
256
257 fn failing(status: Status) -> (impl Fn() -> Opened, Arc<AtomicUsize>) {
259 let calls = Arc::new(AtomicUsize::new(0));
260 let seen = Arc::clone(&calls);
261 (
262 move || {
263 seen.fetch_add(1, Ordering::SeqCst);
264 std::future::ready(Err(status.clone()))
265 },
266 calls,
267 )
268 }
269
270 #[tokio::test(start_paused = true)]
271 async fn a_refusal_for_good_is_reported_once_and_the_supervisor_ends() {
272 let (open, calls) = failing(Status::unauthenticated("40003"));
273 let (tx, mut rx) = mpsc::channel::<String>(8);
274 let last = Arc::new(LastError::default());
275 supervise(
276 "order_state",
277 open,
278 || "connected".to_string(),
279 |_m: u8| "message".to_string(),
280 |reason| format!("failed: {reason}"),
281 tx,
282 Duration::from_secs(5),
283 Arc::clone(&last),
284 CancellationToken::new(),
285 )
286 .await;
287 assert_eq!(calls.load(Ordering::SeqCst), 1, "not tried again");
288 let told = rx.recv().await.unwrap();
289 assert!(told.starts_with("failed: unauthenticated"), "{told}");
290 assert!(rx.recv().await.is_none(), "nothing else was sent");
291 assert!(last.get().unwrap().contains("unauthenticated"));
292 }
293
294 #[tokio::test(start_paused = true)]
295 async fn a_failure_that_may_pass_is_retried_with_a_growing_pause_until_cancelled() {
296 let (open, calls) = failing(Status::unavailable("down"));
297 let (tx, mut rx) = mpsc::channel::<String>(8);
298 let cancel = CancellationToken::new();
299 let last = Arc::new(LastError::default());
300 let task = tokio::spawn(supervise(
301 "trades",
302 open,
303 || "connected".to_string(),
304 |_m: u8| "message".to_string(),
305 |reason| format!("failed: {reason}"),
306 tx,
307 Duration::from_secs(5),
308 Arc::clone(&last),
309 cancel.clone(),
310 ));
311 let settle = || async {
312 for _ in 0..8 {
313 tokio::task::yield_now().await;
314 }
315 };
316 settle().await;
317 assert_eq!(calls.load(Ordering::SeqCst), 1);
318 assert!(last.get().unwrap().contains("open failed"));
319 tokio::time::advance(Duration::from_millis(499)).await;
321 settle().await;
322 assert_eq!(calls.load(Ordering::SeqCst), 1);
323 tokio::time::advance(Duration::from_millis(1)).await;
324 settle().await;
325 assert_eq!(calls.load(Ordering::SeqCst), 2);
326 tokio::time::advance(Duration::from_millis(999)).await;
327 settle().await;
328 assert_eq!(calls.load(Ordering::SeqCst), 2);
329 tokio::time::advance(Duration::from_millis(1)).await;
330 settle().await;
331 assert_eq!(calls.load(Ordering::SeqCst), 3);
332 assert!(rx.try_recv().is_err());
334 cancel.cancel();
335 task.await.unwrap();
336 }
337
338 #[tokio::test]
339 async fn a_supervisor_that_is_cancelled_before_it_starts_does_nothing() {
340 let (open, calls) = failing(Status::unavailable("down"));
341 let (tx, _rx) = mpsc::channel::<String>(1);
342 let cancel = CancellationToken::new();
343 cancel.cancel();
344 supervise(
345 "x",
346 open,
347 || "connected".to_string(),
348 |_m: u8| "message".to_string(),
349 |reason| reason,
350 tx,
351 Duration::from_secs(1),
352 Arc::new(LastError::default()),
353 cancel,
354 )
355 .await;
356 assert_eq!(calls.load(Ordering::SeqCst), 0);
357 }
358}