1use std::num::NonZeroU32;
16use std::ops::ControlFlow;
17use std::sync::Arc;
18use std::time::Duration;
19
20use tokio::time::sleep;
21
22use crate::errors::InvocationError;
23
24pub trait RetryPolicy: Send + Sync + 'static {
31 fn should_retry(&self, ctx: &RetryContext) -> ControlFlow<(), Duration>;
36}
37
38pub struct RetryContext {
40 pub fail_count: NonZeroU32,
42 pub slept_so_far: Duration,
44 pub error: InvocationError,
46}
47
48pub struct NoRetries;
52
53impl RetryPolicy for NoRetries {
54 fn should_retry(&self, _: &RetryContext) -> ControlFlow<(), Duration> {
55 ControlFlow::Break(())
56 }
57}
58
59pub struct AutoSleep {
71 pub threshold: Duration,
75
76 pub io_errors_as_flood_of: Option<Duration>,
79}
80
81impl Default for AutoSleep {
82 fn default() -> Self {
83 Self {
84 threshold: Duration::from_secs(60),
85 io_errors_as_flood_of: Some(Duration::from_secs(1)),
86 }
87 }
88}
89
90fn jitter_duration(base: Duration, seed: u32, max_jitter_secs: u64) -> Duration {
96 let h = {
98 let mut v = seed as u64 ^ 0x9e37_79b9_7f4a_7c15;
99 v ^= v >> 30;
100 v = v.wrapping_mul(0xbf58_476d_1ce4_e5b9);
101 v ^= v >> 27;
102 v = v.wrapping_mul(0x94d0_49bb_1331_11eb);
103 v ^= v >> 31;
104 v
105 };
106 let range_ms = max_jitter_secs * 1000 * 2 + 1;
108 let jitter_ms = (h % range_ms) as i64 - (max_jitter_secs * 1000) as i64;
109 let base_ms = base.as_millis() as i64;
110 let final_ms = (base_ms + jitter_ms).max(0) as u64;
111 Duration::from_millis(final_ms)
112}
113
114impl RetryPolicy for AutoSleep {
115 fn should_retry(&self, ctx: &RetryContext) -> ControlFlow<(), Duration> {
116 match &ctx.error {
117 InvocationError::Rpc(rpc) if rpc.code == 420 && rpc.name == "FLOOD_WAIT" => {
121 let secs = rpc.value.unwrap_or(0) as u64;
122 if secs <= self.threshold.as_secs() {
123 let delay = jitter_duration(Duration::from_secs(secs), ctx.fail_count.get(), 2);
124 tracing::debug!(
125 "[ferogram::retry] FLOOD_WAIT_{secs}: sleeping {delay:?} before retrying"
126 );
127 ControlFlow::Continue(delay)
128 } else {
129 ControlFlow::Break(())
130 }
131 }
132
133 InvocationError::Rpc(rpc) if rpc.code == 420 && rpc.name == "SLOWMODE_WAIT" => {
136 let secs = rpc.value.unwrap_or(0) as u64;
137 if secs <= self.threshold.as_secs() {
138 let delay = jitter_duration(Duration::from_secs(secs), ctx.fail_count.get(), 2);
139 tracing::debug!(
140 "[ferogram::retry] SLOWMODE_WAIT_{secs}: sleeping {delay:?} before retrying"
141 );
142 ControlFlow::Continue(delay)
143 } else {
144 ControlFlow::Break(())
145 }
146 }
147
148 InvocationError::Io(_) if ctx.fail_count.get() <= 1 => {
150 if let Some(d) = self.io_errors_as_flood_of {
151 tracing::debug!(
152 "[ferogram::retry] transient I/O error (attempt {}): sleeping {d:?} before retrying",
153 ctx.fail_count.get()
154 );
155 ControlFlow::Continue(d)
156 } else {
157 ControlFlow::Break(())
158 }
159 }
160
161 _ => ControlFlow::Break(()),
162 }
163 }
164}
165
166pub struct RetryLoop {
189 policy: Arc<dyn RetryPolicy>,
190 ctx: RetryContext,
191}
192
193impl RetryLoop {
194 pub fn new(policy: Arc<dyn RetryPolicy>) -> Self {
197 Self {
198 policy,
199 ctx: RetryContext {
200 fail_count: NonZeroU32::new(1).expect("1 is nonzero"),
201 slept_so_far: Duration::default(),
202 error: InvocationError::Dropped,
203 },
204 }
205 }
206
207 pub async fn advance(&mut self, err: InvocationError) -> Result<(), InvocationError> {
211 self.ctx.error = err;
212 match self.policy.should_retry(&self.ctx) {
213 ControlFlow::Continue(delay) => {
214 sleep(delay).await;
215 self.ctx.slept_so_far += delay;
216 self.ctx.fail_count = self.ctx.fail_count.saturating_add(1);
218 Ok(())
219 }
220 ControlFlow::Break(()) => {
221 Err(std::mem::replace(
223 &mut self.ctx.error,
224 InvocationError::Dropped,
225 ))
226 }
227 }
228 }
229}
230
231#[derive(Debug)]
235enum CbState {
236 Closed { consecutive_failures: u32 },
238 Open { tripped_at: std::time::Instant },
240}
241
242pub struct CircuitBreaker {
268 threshold: u32,
270 cooldown: Duration,
272 state: std::sync::Mutex<CbState>,
273}
274
275impl CircuitBreaker {
276 pub fn new(threshold: u32, cooldown: Duration) -> Self {
281 assert!(
282 threshold >= 1,
283 "CircuitBreaker threshold must be at least 1"
284 );
285 Self {
286 threshold,
287 cooldown,
288 state: std::sync::Mutex::new(CbState::Closed {
289 consecutive_failures: 0,
290 }),
291 }
292 }
293}
294
295impl RetryPolicy for CircuitBreaker {
296 fn should_retry(&self, _ctx: &RetryContext) -> ControlFlow<(), Duration> {
297 let mut state = self.state.lock().expect("lock poisoned");
298 match &*state {
299 CbState::Open { tripped_at } => {
300 if tripped_at.elapsed() >= self.cooldown {
301 *state = CbState::Closed {
303 consecutive_failures: 1,
304 };
305 ControlFlow::Continue(Duration::from_millis(200))
306 } else {
307 ControlFlow::Break(())
309 }
310 }
311 CbState::Closed {
312 consecutive_failures,
313 } => {
314 let new_count = consecutive_failures + 1;
315 if new_count >= self.threshold {
316 tracing::warn!(
317 "[ferogram::retry] circuit breaker tripped after {new_count} consecutive failures; rejecting requests for {:?}",
318 self.cooldown
319 );
320 *state = CbState::Open {
321 tripped_at: std::time::Instant::now(),
322 };
323 ControlFlow::Break(())
324 } else {
325 let backoff_ms = 200u64 * (1u64 << new_count.saturating_sub(1).min(4));
327 *state = CbState::Closed {
328 consecutive_failures: new_count,
329 };
330 ControlFlow::Continue(Duration::from_millis(backoff_ms))
331 }
332 }
333 }
334 }
335}
336
337#[cfg(test)]
340mod tests {
341 use super::*;
342 use crate::errors::RpcError;
343 use std::io;
344
345 fn flood(secs: u32) -> InvocationError {
346 InvocationError::Rpc(RpcError {
347 code: 420,
348 name: "FLOOD_WAIT".into(),
349 value: Some(secs),
350 })
351 }
352
353 fn io_err() -> InvocationError {
354 InvocationError::Io(io::Error::new(io::ErrorKind::ConnectionReset, "reset"))
355 }
356
357 fn rpc(code: i32, name: &str, value: Option<u32>) -> InvocationError {
358 InvocationError::Rpc(RpcError {
359 code,
360 name: name.into(),
361 value,
362 })
363 }
364
365 #[test]
368 fn no_retries_always_breaks() {
369 let policy = NoRetries;
370 let ctx = RetryContext {
371 fail_count: NonZeroU32::new(1).expect("1 is nonzero"),
372 slept_so_far: Duration::default(),
373 error: flood(10),
374 };
375 assert!(matches!(policy.should_retry(&ctx), ControlFlow::Break(())));
376 }
377
378 #[test]
381 fn autosleep_retries_flood_under_threshold() {
382 let policy = AutoSleep::default(); let ctx = RetryContext {
384 fail_count: NonZeroU32::new(1).expect("1 is nonzero"),
385 slept_so_far: Duration::default(),
386 error: flood(30),
387 };
388 match policy.should_retry(&ctx) {
389 ControlFlow::Continue(d) => {
391 let secs = d.as_secs_f64();
392 assert!(
393 secs >= 28.0 && secs <= 32.0,
394 "expected 28-32s delay (jitter), got {secs:.3}s"
395 );
396 }
397 other => panic!("expected Continue, got {other:?}"),
398 }
399 }
400
401 #[test]
402 fn autosleep_breaks_flood_over_threshold() {
403 let policy = AutoSleep::default(); let ctx = RetryContext {
405 fail_count: NonZeroU32::new(1).expect("1 is nonzero"),
406 slept_so_far: Duration::default(),
407 error: flood(120),
408 };
409 assert!(matches!(policy.should_retry(&ctx), ControlFlow::Break(())));
410 }
411
412 #[test]
413 fn autosleep_second_flood_retry_is_honoured() {
414 let policy = AutoSleep::default();
415 let ctx = RetryContext {
416 fail_count: NonZeroU32::new(2).expect("2 is nonzero"),
417 slept_so_far: Duration::from_secs(30),
418 error: flood(30),
419 };
420 match policy.should_retry(&ctx) {
421 ControlFlow::Continue(d) => {
423 let secs = d.as_secs_f64();
424 assert!(
425 secs >= 28.0 && secs <= 32.0,
426 "expected 28-32s on second FLOOD_WAIT, got {secs:.3}s"
427 );
428 }
429 other => panic!("expected Continue on second FLOOD_WAIT, got {other:?}"),
430 }
431 }
432
433 #[test]
434 fn autosleep_retries_io_once() {
435 let policy = AutoSleep::default();
436 let ctx = RetryContext {
437 fail_count: NonZeroU32::new(1).expect("1 is nonzero"),
438 slept_so_far: Duration::default(),
439 error: io_err(),
440 };
441 match policy.should_retry(&ctx) {
442 ControlFlow::Continue(d) => assert_eq!(d, Duration::from_secs(1)),
443 other => panic!("expected Continue, got {other:?}"),
444 }
445 }
446
447 #[test]
448 fn autosleep_no_io_retry_after_first() {
449 let policy = AutoSleep::default();
450 let ctx = RetryContext {
451 fail_count: NonZeroU32::new(4).expect("4 is nonzero"),
452 slept_so_far: Duration::from_secs(3),
453 error: io_err(),
454 };
455 assert!(matches!(policy.should_retry(&ctx), ControlFlow::Break(())));
456 }
457
458 #[test]
459 fn autosleep_breaks_other_rpc() {
460 let policy = AutoSleep::default();
461 let ctx = RetryContext {
462 fail_count: NonZeroU32::new(1).expect("1 is nonzero"),
463 slept_so_far: Duration::default(),
464 error: rpc(400, "BAD_REQUEST", None),
465 };
466 assert!(matches!(policy.should_retry(&ctx), ControlFlow::Break(())));
467 }
468
469 #[test]
472 fn migrate_dc_id_detected() {
473 let e = RpcError {
474 code: 303,
475 name: "PHONE_MIGRATE".into(),
476 value: Some(5),
477 };
478 assert_eq!(e.migrate_dc_id(), Some(5));
479 }
480
481 #[test]
482 fn network_migrate_detected() {
483 let e = RpcError {
484 code: 303,
485 name: "NETWORK_MIGRATE".into(),
486 value: Some(3),
487 };
488 assert_eq!(e.migrate_dc_id(), Some(3));
489 }
490
491 #[test]
492 fn file_migrate_detected() {
493 let e = RpcError {
494 code: 303,
495 name: "FILE_MIGRATE".into(),
496 value: Some(4),
497 };
498 assert_eq!(e.migrate_dc_id(), Some(4));
499 }
500
501 #[test]
502 fn non_migrate_is_none() {
503 let e = RpcError {
504 code: 420,
505 name: "FLOOD_WAIT".into(),
506 value: Some(30),
507 };
508 assert_eq!(e.migrate_dc_id(), None);
509 }
510
511 #[test]
512 fn migrate_falls_back_to_dc2_when_no_value() {
513 let e = RpcError {
514 code: 303,
515 name: "PHONE_MIGRATE".into(),
516 value: None,
517 };
518 assert_eq!(e.migrate_dc_id(), Some(2));
519 }
520
521 #[tokio::test]
524 async fn retry_loop_gives_up_on_no_retries() {
525 let mut rl = RetryLoop::new(Arc::new(NoRetries));
526 let err = rpc(400, "SOMETHING_WRONG", None);
527 let result = rl.advance(err).await;
528 assert!(result.is_err());
529 }
530
531 #[tokio::test]
532 async fn retry_loop_increments_fail_count() {
533 let mut rl = RetryLoop::new(Arc::new(AutoSleep {
534 threshold: Duration::from_secs(60),
535 io_errors_as_flood_of: Some(Duration::from_millis(1)),
536 }));
537 assert!(rl.advance(io_err()).await.is_ok());
538 assert!(rl.advance(io_err()).await.is_err());
539 }
540
541 #[test]
544 fn circuit_breaker_trips_after_threshold() {
545 let cb = CircuitBreaker::new(3, Duration::from_secs(60));
546 let ctx = |n: u32| RetryContext {
547 fail_count: NonZeroU32::new(n).unwrap(),
548 slept_so_far: Duration::default(),
549 error: rpc(500, "INTERNAL", None),
550 };
551 assert!(matches!(cb.should_retry(&ctx(1)), ControlFlow::Continue(_)));
553 assert!(matches!(cb.should_retry(&ctx(2)), ControlFlow::Continue(_)));
554 assert!(matches!(cb.should_retry(&ctx(3)), ControlFlow::Break(())));
556 assert!(matches!(cb.should_retry(&ctx(4)), ControlFlow::Break(())));
558 }
559
560 #[test]
561 fn circuit_breaker_resets_after_cooldown() {
562 let cb = CircuitBreaker::new(2, Duration::from_millis(10));
563 let ctx = |n: u32| RetryContext {
564 fail_count: NonZeroU32::new(n).unwrap(),
565 slept_so_far: Duration::default(),
566 error: rpc(500, "INTERNAL", None),
567 };
568 assert!(matches!(cb.should_retry(&ctx(1)), ControlFlow::Continue(_)));
570 assert!(matches!(cb.should_retry(&ctx(2)), ControlFlow::Break(())));
571 std::thread::sleep(Duration::from_millis(20));
573 assert!(matches!(cb.should_retry(&ctx(1)), ControlFlow::Continue(_)));
575 }
576}