Skip to main content

duckduckgo_core/rate_limit/
progress.rs

1//! Public types backing the `RateLimiter` progress hook. The CLI
2//! installs a closure of type [`ProgressHook`] which the limiter calls
3//! before each cooldown / spacing sleep so the user can see a
4//! transparent "[INFO] rate-limit ..." line on stderr while the
5//! request is gated. Library callers that don't need progress
6//! reporting can ignore this module entirely; the hook is opt-in.
7
8use std::sync::Arc;
9use std::time::Duration;
10
11/// Which kind of wait the limiter is currently performing. Surfaces in
12/// `RateLimitProgress` events so callers can render distinct user-facing
13/// messages for the post-block cooldown vs. the inter-request spacing
14/// gap.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum RateLimitWait {
17    /// Post-block cooldown after a 202/403/429/anomaly response.
18    Cooldown,
19    /// Inter-request spacing (`base_spacing` or `slow_spacing`).
20    Spacing,
21}
22
23impl RateLimitWait {
24    /// Stable lowercase token suitable for short user-facing messages.
25    pub fn as_token(&self) -> &'static str {
26        match self {
27            Self::Cooldown => "cooldown",
28            Self::Spacing => "spacing",
29        }
30    }
31}
32
33/// One progress observation for an in-flight rate-limit wait. Emitted
34/// once per acquire cycle (the inner sleep is bounded so a long
35/// cooldown still yields periodic updates without a separate timer
36/// task).
37///
38/// `total ≈ elapsed + remaining` at the moment of emission, modulo
39/// jitter and small clock drift.
40#[derive(Clone, Copy, Debug)]
41pub struct RateLimitProgress {
42    pub kind: RateLimitWait,
43    pub elapsed: Duration,
44    pub remaining: Duration,
45    pub total: Duration,
46    pub consecutive_blocks: u32,
47}
48
49/// Optional callback the limiter invokes before each wait sleep so a
50/// host (typically the CLI) can render a transparent progress line. The
51/// closure must be cheap; it is called while holding no locks but on
52/// the hot path of the limiter loop.
53pub type ProgressHook = Arc<dyn Fn(RateLimitProgress) + Send + Sync>;
54
55#[cfg(test)]
56mod tests {
57    use super::RateLimitWait;
58
59    #[test]
60    fn wait_tokens_are_stable() {
61        assert_eq!(RateLimitWait::Cooldown.as_token(), "cooldown");
62        assert_eq!(RateLimitWait::Spacing.as_token(), "spacing");
63    }
64}