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.
This commit is contained in:
Jon Seager 2026-02-10 17:15:46 +00:00
parent 3675a2cdc6
commit dfd9814fa5
No known key found for this signature in database
2 changed files with 17 additions and 0 deletions

View file

@ -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,

View file

@ -82,6 +82,7 @@ pub struct AppState {
pub cup_service: CupService,
pub insecure_cookies: bool,
pub stats_invalidator: StatsInvalidator,
pub image_semaphore: Arc<tokio::sync::Semaphore>,
}
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)),
}
}
}