game_manager_lib/services/
rate_limiter.rs1use crate::constants::{
9 RAWG_BACKOFF_BASE_MS, RAWG_BACKOFF_MAX_RETRIES, RAWG_MAX_CONCURRENT_REQUESTS,
10 STEAM_BACKOFF_BASE_MS, STEAM_BACKOFF_MAX_RETRIES, STEAM_MAX_CONCURRENT_REQUESTS,
11};
12use lazy_static::lazy_static;
13use std::sync::Arc;
14use std::time::Duration;
15use tokio::sync::Semaphore;
16use tokio::time::sleep;
17use tracing::warn;
18
19pub struct ApiRateLimiter {
20 semaphore: Arc<Semaphore>,
21 max_retries: u32,
22 backoff_base_ms: u64,
23 name: &'static str, }
25
26impl ApiRateLimiter {
27 pub fn new(
28 max_concurrent: usize,
29 max_retries: u32,
30 backoff_base_ms: u64,
31 name: &'static str,
32 ) -> Self {
33 Self {
34 semaphore: Arc::new(Semaphore::new(max_concurrent)),
35 max_retries,
36 backoff_base_ms,
37 name,
38 }
39 }
40
41 pub async fn run<F, Fut, T>(&self, call: F) -> Result<T, String>
46 where
47 F: Fn() -> Fut,
48 Fut: std::future::Future<Output=Result<T, String>>,
49 {
50 let _permit = self.semaphore.acquire().await.map_err(|e| e.to_string())?;
51
52 let mut attempt = 0;
53 loop {
54 match call().await {
55 Ok(result) => return Ok(result),
56 Err(e) if is_throttling_error(&e) && attempt < self.max_retries => {
57 attempt += 1;
58 let backoff = self.backoff_base_ms * 2u64.pow(attempt - 1);
59 warn!(
60 "{}: throttling detectado, tentativa {}/{}, aguardando {}ms — {}",
61 self.name, attempt, self.max_retries, backoff, e
62 );
63 sleep(Duration::from_millis(backoff)).await;
64 }
65 Err(e) => return Err(e),
66 }
67 }
68 }
69}
70
71lazy_static! {
72 pub static ref RAWG_LIMITER: ApiRateLimiter = ApiRateLimiter::new(
73 RAWG_MAX_CONCURRENT_REQUESTS as usize,
74 RAWG_BACKOFF_MAX_RETRIES,
75 RAWG_BACKOFF_BASE_MS,
76 "RAWG",
77 );
78 pub static ref STEAM_LIMITER: ApiRateLimiter = ApiRateLimiter::new(
79 STEAM_MAX_CONCURRENT_REQUESTS as usize,
80 STEAM_BACKOFF_MAX_RETRIES,
81 STEAM_BACKOFF_BASE_MS,
82 "Steam",
83 );
84}
85
86fn is_throttling_error(err: &str) -> bool {
92 err.contains("429")
93 || err.contains("403")
94 || err.contains("502")
95 || err.contains("503")
96 || err.contains("504")
97}