1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//! Per-provider cooldown registry.
//!
//! A 429 that declares a wait parks the provider until that moment. The
//! fallback chain routes around a parked provider, and it re-enters on its
//! own once the window lapses. A registry instance per chain, not a global,
//! so parallel tests never share state.
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::Instant;
#[derive(Default)]
pub struct CooldownRegistry {
until: Mutex<HashMap<String, Instant>>,
}
impl CooldownRegistry {
/// Park `provider` until `until`. A later, longer 429 extends the window;
/// an earlier one never shortens it.
pub fn park(&self, provider: &str, until: Instant) {
let mut map = self.until.lock().unwrap_or_else(|p| p.into_inner());
let slot = map.entry(provider.to_string()).or_insert(until);
if until > *slot {
*slot = until;
}
}
/// True while the provider's declared window is still open.
pub fn is_cooling(&self, provider: &str) -> bool {
let map = self.until.lock().unwrap_or_else(|p| p.into_inner());
map.get(provider)
.is_some_and(|until| *until > Instant::now())
}
/// True when a window was ever recorded, cooling or lapsed. A lapsed record
/// is what lets the chain try the provider again. Records are never
/// removed: there is one per provider name, so the map stays bounded.
pub fn has_record(&self, provider: &str) -> bool {
let map = self.until.lock().unwrap_or_else(|p| p.into_inner());
map.contains_key(provider)
}
}