diff --git a/apps/labrinth/src/routes/internal/campaign.rs b/apps/labrinth/src/routes/internal/campaign.rs index 34a6e9fe7..88f22b548 100644 --- a/apps/labrinth/src/routes/internal/campaign.rs +++ b/apps/labrinth/src/routes/internal/campaign.rs @@ -60,15 +60,24 @@ struct TiltifyMeta { subscription_source_type: String, } -#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)] pub struct CampaignInfo { total_donations_usd: Decimal, target_usd: Decimal, num_donators: usize, + cached_at: DateTime, } const CAMPAIGN_INFO_CACHE_NAMESPACE: &str = "campaign_info"; -const CAMPAIGN_INFO_CACHE_TTL_SECONDS: i64 = 15 * 60; +const CAMPAIGN_INFO_CACHE_STALE_SECONDS: i64 = 15 * 60; +const CAMPAIGN_INFO_CACHE_TTL_SECONDS: i64 = 24 * 60 * 60; + +impl CampaignInfo { + fn is_stale(&self) -> bool { + Utc::now().signed_duration_since(self.cached_at) + >= Duration::seconds(CAMPAIGN_INFO_CACHE_STALE_SECONDS) + } +} #[derive(Debug, Deserialize)] struct TiltifyCampaignResponse { @@ -305,66 +314,89 @@ pub async fn pride_26( .await .wrap_internal_err("connecting to redis")?; - if let Some(cached) = redis_connection - .get(CAMPAIGN_INFO_CACHE_NAMESPACE, campaign_id) - .await - .wrap_internal_err("getting cached campaign info")? - { - let campaign_info = serde_json::from_str::(&cached) - .wrap_internal_err("parsing cached campaign info")?; - return Ok(web::Json(campaign_info)); - } - - let access_token = tiltify - .access_token() - .await - .wrap_internal_err("fetching Tiltify access token")?; - let url = format!( - "https://v5api.tiltify.com/api/public/team_campaigns/{campaign_id}", - ); - let response = http - .get(url) - .bearer_auth(&access_token) - .send() - .await - .wrap_internal_err("fetching campaign from Tiltify")? - .error_for_status() - .wrap_internal_err("fetching campaign from Tiltify")? - .json::() - .await - .wrap_internal_err("parsing Tiltify response")?; - - let raised_currency = &response.data.total_amount_raised.currency; - if raised_currency != "USD" { - return Err(ApiError::Internal(eyre!( - "total amount raised is in {raised_currency}, must be USD" - ))); - } - - let goal_currency = &response.data.goal.currency; - if goal_currency != "USD" { - return Err(ApiError::Internal(eyre!( - "goal amount is in {goal_currency}, must be USD" - ))); - } - - let campaign_info = CampaignInfo { - total_donations_usd: response.data.total_amount_raised.value, - target_usd: response.data.goal.value, - num_donators: num_donators(&http, &access_token, campaign_id).await?, - }; - - redis_connection - .set_serialized_to_json( + let cached = redis_connection + .get_deserialized_from_json::( CAMPAIGN_INFO_CACHE_NAMESPACE, campaign_id, - &campaign_info, - Some(CAMPAIGN_INFO_CACHE_TTL_SECONDS), ) .await - .wrap_internal_err("caching campaign info")?; + .wrap_internal_err("getting cached campaign info")?; - Ok(web::Json(campaign_info)) + if let Some(cached) = &cached + && !cached.is_stale() + { + return Ok(web::Json(cached.clone())); + } + + let result = async { + let access_token = tiltify + .access_token() + .await + .wrap_internal_err("fetching Tiltify access token")?; + let url = format!( + "https://v5api.tiltify.com/api/public/team_campaigns/{campaign_id}", + ); + let response = http + .get(url) + .bearer_auth(&access_token) + .send() + .await + .wrap_internal_err("fetching campaign from Tiltify")? + .error_for_status() + .wrap_internal_err("fetching campaign from Tiltify")? + .json::() + .await + .wrap_internal_err("parsing Tiltify response")?; + + let raised_currency = &response.data.total_amount_raised.currency; + if raised_currency != "USD" { + return Err(ApiError::Internal(eyre!( + "total amount raised is in {raised_currency}, must be USD" + ))); + } + + let goal_currency = &response.data.goal.currency; + if goal_currency != "USD" { + return Err(ApiError::Internal(eyre!( + "goal amount is in {goal_currency}, must be USD" + ))); + } + + let campaign_info = CampaignInfo { + total_donations_usd: response.data.total_amount_raised.value, + target_usd: response.data.goal.value, + num_donators: num_donators(&http, &access_token, campaign_id) + .await?, + cached_at: Utc::now(), + }; + + redis_connection + .set_serialized_to_json( + CAMPAIGN_INFO_CACHE_NAMESPACE, + campaign_id, + &campaign_info, + Some(CAMPAIGN_INFO_CACHE_TTL_SECONDS), + ) + .await + .wrap_internal_err("caching campaign info")?; + + Ok(campaign_info) + } + .await; + + match result { + Ok(campaign_info) => Ok(web::Json(campaign_info)), + Err(error) => { + if let Some(cached) = cached { + warn!( + "Failed to refresh campaign info from Tiltify: {error:?}" + ); + Ok(web::Json(cached)) + } else { + Err(error) + } + } + } } async fn num_donators(