Serve stale Tiltify data if API is not available (#6427)

This commit is contained in:
aecsocket
2026-06-17 15:58:07 +01:00
committed by GitHub
parent 5ed322d281
commit 2bb1ef775c
+89 -57
View File
@@ -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<Utc>,
}
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::<CampaignInfo>(&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::<TiltifyCampaignResponse>()
.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::<CampaignInfo>(
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::<TiltifyCampaignResponse>()
.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(