fc-common: GitHub rate limit state extraction
Extract `X-RateLimit-*` headers from responses and calculate an adaptive delay. Minimum delay is 1 seconds to prevent division by 0. Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ib35b0d0e720098e2c68ced88a8821c7b6a6a6964
This commit is contained in:
parent
f5c16aef83
commit
4050de5b4e
3 changed files with 84 additions and 0 deletions
|
|
@ -18,6 +18,29 @@ fn http_client() -> &'static reqwest::Client {
|
|||
CLIENT.get_or_init(reqwest::Client::new)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RateLimitState {
|
||||
pub limit: u64,
|
||||
pub remaining: u64,
|
||||
pub reset_at: u64,
|
||||
}
|
||||
|
||||
pub fn extract_rate_limit_from_headers(
|
||||
headers: &reqwest::header::HeaderMap,
|
||||
) -> Option<RateLimitState> {
|
||||
let limit = headers.get("X-RateLimit-Limit")?.to_str().ok()?.parse().ok()?;
|
||||
let remaining = headers.get("X-RateLimit-Remaining")?.to_str().ok()?.parse().ok()?;
|
||||
let reset_at = headers.get("X-RateLimit-Reset")?.to_str().ok()?.parse().ok()?;
|
||||
Some(RateLimitState { limit, remaining, reset_at })
|
||||
}
|
||||
|
||||
pub fn calculate_delay(state: &RateLimitState, now: u64) -> u64 {
|
||||
let seconds_until_reset = state.reset_at.saturating_sub(now).max(1);
|
||||
let consumed = state.limit.saturating_sub(state.remaining);
|
||||
let delay = (consumed * 5) / seconds_until_reset;
|
||||
delay.max(1)
|
||||
}
|
||||
|
||||
/// Dispatch all configured notifications for a completed build.
|
||||
/// If retry queue is enabled, enqueues tasks; otherwise sends immediately.
|
||||
pub async fn dispatch_build_finished(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
//! Integration tests for database and configuration
|
||||
|
||||
mod notifications_tests;
|
||||
|
||||
use fc_common::{
|
||||
Database,
|
||||
config::{Config, DatabaseConfig},
|
||||
|
|
|
|||
59
crates/common/tests/notifications_tests.rs
Normal file
59
crates/common/tests/notifications_tests.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
use fc_common::notifications::*;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[test]
|
||||
fn test_rate_limit_extraction() {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("X-RateLimit-Limit", "5000".parse().unwrap());
|
||||
headers.insert("X-RateLimit-Remaining", "1234".parse().unwrap());
|
||||
headers.insert("X-RateLimit-Reset", "1735689600".parse().unwrap());
|
||||
|
||||
let state = extract_rate_limit_from_headers(&headers);
|
||||
assert!(state.is_some());
|
||||
|
||||
let state = state.unwrap();
|
||||
assert_eq!(state.limit, 5000);
|
||||
assert_eq!(state.remaining, 1234);
|
||||
assert_eq!(state.reset_at, 1735689600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rate_limit_missing_headers() {
|
||||
let headers = reqwest::header::HeaderMap::new();
|
||||
let state = extract_rate_limit_from_headers(&headers);
|
||||
assert!(state.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sleep_duration_calculation() {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
let state = RateLimitState {
|
||||
limit: 5000,
|
||||
remaining: 500,
|
||||
reset_at: now + 3600,
|
||||
};
|
||||
|
||||
let delay = calculate_delay(&state, now);
|
||||
assert!(delay >= 6 && delay <= 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sleep_duration_minimum() {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
let state = RateLimitState {
|
||||
limit: 5000,
|
||||
remaining: 4999,
|
||||
reset_at: now + 10000,
|
||||
};
|
||||
|
||||
let delay = calculate_delay(&state, now);
|
||||
assert_eq!(delay, 1);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue