use std::sync::RwLock;
use std::time::{Duration, Instant};
static GLOBAL_COOLDOWN: RwLock<Option<Instant>> = RwLock::new(None);
pub(crate) fn parse_retry_after(error: &str) -> Option<Duration> {
let lower = error.to_lowercase();
let idx = lower.find("retry after")?;
let rest = &lower[idx + "retry after".len()..];
let digits: String = rest
.chars()
.skip_while(|c| !c.is_ascii_digit())
.take_while(char::is_ascii_digit)
.collect();
if digits.is_empty() {
return None;
}
let secs: u64 = digits.parse().ok()?;
(secs > 0).then(|| Duration::from_secs(secs))
}
pub(crate) const RETRY_MARGIN: Duration = Duration::from_secs(2);
pub(crate) const MAX_INLINE_RATE_LIMIT_WAIT: Duration = Duration::from_secs(30);
pub(crate) fn clamp_inline_wait(requested: Duration) -> (Duration, bool) {
if requested > MAX_INLINE_RATE_LIMIT_WAIT {
(MAX_INLINE_RATE_LIMIT_WAIT, true)
} else {
(requested, false)
}
}
pub(crate) fn record_global_429(retry_after: Duration) {
let (wait, capped) = clamp_inline_wait(retry_after);
let total_wait = wait + RETRY_MARGIN;
let now = super::governor::gate_now();
let new_deadline = now + total_wait;
let mut lock = GLOBAL_COOLDOWN.write().unwrap_or_else(|e| e.into_inner());
let active_deadline = match *lock {
Some(existing) if existing > new_deadline => existing,
_ => {
*lock = Some(new_deadline);
new_deadline
}
};
if capped {
tracing::warn!(
"Telegram: Global 429 cooldown activated: {}s requested exceeds {}s cap \
— cooling down for {}s (deadline {:?}); chat likely flood-banned",
retry_after.as_secs(),
MAX_INLINE_RATE_LIMIT_WAIT.as_secs(),
total_wait.as_secs(),
active_deadline
);
} else {
tracing::warn!(
"Telegram: Global 429 cooldown activated: cooling down for {}s (deadline {:?})",
total_wait.as_secs(),
active_deadline
);
}
}
pub(crate) fn is_global_cooldown_active() -> bool {
let now = super::governor::gate_now();
let lock = GLOBAL_COOLDOWN.read().unwrap_or_else(|e| e.into_inner());
match *lock {
Some(deadline) => deadline > now,
None => false,
}
}
pub(crate) async fn wait_global_cooldown() -> Duration {
let now = super::governor::gate_now();
let remaining = {
let lock = GLOBAL_COOLDOWN.read().unwrap_or_else(|e| e.into_inner());
match *lock {
Some(deadline) if deadline > now => Some(deadline.duration_since(now)),
_ => None,
}
};
if let Some(wait) = remaining {
#[cfg(test)]
super::governor::test_support::advance(wait.as_millis() as u64);
tokio::time::sleep(wait).await;
wait
} else {
Duration::ZERO
}
}
#[cfg(test)]
pub(crate) fn reset_global_cooldown() {
let mut lock = GLOBAL_COOLDOWN.write().unwrap_or_else(|e| e.into_inner());
*lock = None;
}
pub(crate) async fn wait_out(what: &str, window: Duration, extra: &str) {
record_global_429(window);
let (wait, capped) = clamp_inline_wait(window);
if capped {
tracing::warn!(
"Telegram: {what} rate-limited{extra}: {}s window exceeds {}s inline cap \
— waiting {}s; capped inline, chat likely flood-banned (#1064)",
window.as_secs(),
MAX_INLINE_RATE_LIMIT_WAIT.as_secs(),
wait.as_secs()
);
} else {
tracing::warn!(
"Telegram: {what} rate-limited{extra} — waiting {}s",
wait.as_secs()
);
}
#[cfg(test)]
super::governor::test_support::advance(wait.as_millis() as u64);
tokio::time::sleep(wait).await;
}