From ac5daad280ffb8b08e4c6c60fe2d52974469dee9 Mon Sep 17 00:00:00 2001 From: aecsocket <43144841+aecsocket@users.noreply.github.com> Date: Fri, 19 Jun 2026 20:02:22 +0100 Subject: [PATCH] Tiltify API query backoff (#6447) * Tiltify API querying backoff * fix backoff * fix clippy * fix2 --- apps/labrinth/src/util/tiltify.rs | 81 ++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 24 deletions(-) diff --git a/apps/labrinth/src/util/tiltify.rs b/apps/labrinth/src/util/tiltify.rs index 95631070c..1ade81024 100644 --- a/apps/labrinth/src/util/tiltify.rs +++ b/apps/labrinth/src/util/tiltify.rs @@ -1,6 +1,6 @@ use std::time::{Duration, Instant}; -use eyre::eyre; +use eyre::{Result, eyre}; use serde::Deserialize; use serde_json::json; use tokio::sync::Mutex; @@ -13,7 +13,24 @@ use crate::{ #[derive(Debug)] pub struct TiltifyClient { http: HttpClient, - token: Mutex>, + state: Mutex, +} + +#[derive(Debug)] +struct TiltifyState { + token: Option, + rate_limited_until: Instant, + rate_limit_backoff: Duration, +} + +impl TiltifyState { + fn set_fetch_backoff(&mut self) { + self.rate_limited_until = Instant::now() + self.rate_limit_backoff; + self.rate_limit_backoff = self + .rate_limit_backoff + .saturating_mul(2) + .min(TILTIFY_MAX_RATE_LIMIT_BACKOFF); + } } #[derive(Debug)] @@ -28,38 +45,34 @@ struct TiltifyTokenResponse { expires_in: u64, } +const TILTIFY_INITIAL_RATE_LIMIT_BACKOFF: Duration = Duration::from_secs(60); +const TILTIFY_MAX_RATE_LIMIT_BACKOFF: Duration = Duration::from_secs(15 * 60); + impl TiltifyClient { pub fn new(http: HttpClient) -> Self { Self { http, - token: Mutex::new(None), + state: Mutex::new(TiltifyState { + token: None, + rate_limited_until: Instant::now(), + rate_limit_backoff: TILTIFY_INITIAL_RATE_LIMIT_BACKOFF, + }), } } - pub async fn access_token(&self) -> eyre::Result { - let mut token = self.token.lock().await; + pub async fn access_token(&self) -> Result { + let mut state = self.state.lock().await; - if let Some(token) = token.as_ref() + if let Some(token) = state.token.as_ref() && token.expires_at > Instant::now() { return Ok(token.access_token.clone()); } - let response = self.fetch_access_token().await?; - let expires_at = Instant::now() - + Duration::from_secs(response.expires_in) - .saturating_sub(Duration::from_secs(60)); - let access_token = response.access_token; + if state.rate_limited_until > Instant::now() { + return Err(eyre!("waiting for rate limit to reset")); + } - *token = Some(TiltifyAccessToken { - access_token: access_token.clone(), - expires_at, - }); - - Ok(access_token) - } - - async fn fetch_access_token(&self) -> eyre::Result { if ENV.TILTIFY_CLIENT_ID.is_empty() || ENV.TILTIFY_CLIENT_SECRET.is_empty() { @@ -79,13 +92,33 @@ impl TiltifyClient { })) .send() .await - .wrap_err("fetching OAuth token")? - .error_for_status() - .wrap_err("fetching OAuth token")? + .inspect_err(|_| state.set_fetch_backoff()) + .wrap_err("fetching OAuth token")?; + + let response = match response.error_for_status() { + Ok(response) => response, + Err(error) => { + state.set_fetch_backoff(); + return Err(error).wrap_err("fetching OAuth token"); + } + }; + + let response = response .json::() .await .wrap_err("parsing OAuth token response")?; - Ok(response) + let expires_at = Instant::now() + + Duration::from_secs(response.expires_in) + .saturating_sub(Duration::from_secs(60)); + let access_token = response.access_token; + + state.token = Some(TiltifyAccessToken { + access_token: access_token.clone(), + expires_at, + }); + state.rate_limit_backoff = TILTIFY_INITIAL_RATE_LIMIT_BACKOFF; + + Ok(access_token) } }