atlassian_cli_api/
ratelimit.rs

1use chrono::{DateTime, Utc};
2use reqwest::Response;
3use std::sync::Arc;
4use tokio::sync::Mutex;
5use tracing::{debug, warn};
6
7#[derive(Clone)]
8pub struct RateLimiter {
9    state: Arc<Mutex<RateLimitState>>,
10}
11
12struct RateLimitState {
13    remaining: Option<u32>,
14    reset_at: Option<DateTime<Utc>>,
15    limit: Option<u32>,
16}
17
18impl RateLimiter {
19    pub fn new() -> Self {
20        Self {
21            state: Arc::new(Mutex::new(RateLimitState {
22                remaining: None,
23                reset_at: None,
24                limit: None,
25            })),
26        }
27    }
28
29    pub async fn update_from_response(&self, response: &Response) {
30        let mut state = self.state.lock().await;
31
32        if let Some(limit) = response.headers().get("x-ratelimit-limit") {
33            if let Ok(s) = limit.to_str() {
34                if let Ok(val) = s.parse() {
35                    state.limit = Some(val);
36                }
37            }
38        }
39
40        if let Some(remaining) = response.headers().get("x-ratelimit-remaining") {
41            if let Ok(s) = remaining.to_str() {
42                if let Ok(val) = s.parse() {
43                    state.remaining = Some(val);
44                }
45            }
46        }
47
48        if let Some(reset) = response.headers().get("x-ratelimit-reset") {
49            if let Ok(s) = reset.to_str() {
50                if let Ok(timestamp) = s.parse::<i64>() {
51                    if let Some(dt) = DateTime::from_timestamp(timestamp, 0) {
52                        state.reset_at = Some(dt);
53                    }
54                }
55            }
56        }
57
58        if let (Some(remaining), Some(limit)) = (state.remaining, state.limit) {
59            let usage_pct = ((limit - remaining) as f64 / limit as f64) * 100.0;
60            if usage_pct > 80.0 {
61                warn!(
62                    remaining,
63                    limit,
64                    usage_pct = format!("{:.1}%", usage_pct),
65                    "Rate limit usage high"
66                );
67            }
68        }
69    }
70
71    pub async fn check_limit(&self) -> Option<u64> {
72        let state = self.state.lock().await;
73
74        if let Some(remaining) = state.remaining {
75            if remaining == 0 {
76                if let Some(reset_at) = state.reset_at {
77                    let now = Utc::now();
78                    if reset_at > now {
79                        let wait_secs = (reset_at - now).num_seconds() as u64;
80                        debug!(wait_secs, "Rate limit exceeded, need to wait");
81                        return Some(wait_secs);
82                    }
83                }
84            }
85        }
86
87        None
88    }
89
90    pub async fn get_info(&self) -> RateLimitInfo {
91        let state = self.state.lock().await;
92        RateLimitInfo {
93            limit: state.limit,
94            remaining: state.remaining,
95            reset_at: state.reset_at,
96        }
97    }
98}
99
100impl Default for RateLimiter {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106#[derive(Debug, Clone)]
107pub struct RateLimitInfo {
108    pub limit: Option<u32>,
109    pub remaining: Option<u32>,
110    pub reset_at: Option<DateTime<Utc>>,
111}