From dfd9814fa5a4ec01f39ef79e88c4d5fb38f3669d Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Tue, 10 Feb 2026 17:15:46 +0000 Subject: [PATCH] fix: add concurrency semaphore for image processing Limit concurrent image processing tasks to 4 via a tokio::Semaphore on AppState. Acquired before spawn_blocking in both upload_image() and save_deferred_image() to prevent CPU/memory exhaustion from concurrent image uploads. --- src/application/routes/api/images.rs | 15 +++++++++++++++ src/application/state.rs | 2 ++ 2 files changed, 17 insertions(+) diff --git a/src/application/routes/api/images.rs b/src/application/routes/api/images.rs index b53ad7a..2ab14d9 100644 --- a/src/application/routes/api/images.rs +++ b/src/application/routes/api/images.rs @@ -105,6 +105,12 @@ pub(crate) async fn upload_image( let (upload, _source) = payload.into_parts(); + let _permit = state + .image_semaphore + .acquire() + .await + .map_err(|_| AppError::unexpected("image processing unavailable"))?; + let image_data = upload.image; let processed = tokio::task::spawn_blocking(move || process_data_url(&image_data)) .await @@ -235,6 +241,15 @@ pub(crate) async fn save_deferred_image( let Some(data_url) = data_url.filter(|s| !s.is_empty()) else { return; }; + let Ok(_permit) = state.image_semaphore.acquire().await else { + tracing::warn!( + entity_type, + entity_id, + "image semaphore closed, skipping deferred image" + ); + return; + }; + let data_url = data_url.to_string(); let processed = match tokio::task::spawn_blocking(move || process_data_url(&data_url)).await { Ok(Ok(p)) => p, diff --git a/src/application/state.rs b/src/application/state.rs index f525b92..42726ef 100644 --- a/src/application/state.rs +++ b/src/application/state.rs @@ -82,6 +82,7 @@ pub struct AppState { pub cup_service: CupService, pub insecure_cookies: bool, pub stats_invalidator: StatsInvalidator, + pub image_semaphore: Arc, } impl AppState { @@ -172,6 +173,7 @@ impl AppState { cup_service, insecure_cookies: config.insecure_cookies, stats_invalidator: config.stats_invalidator, + image_semaphore: Arc::new(tokio::sync::Semaphore::new(4)), } } }