fix: move image processing off async runtime with spawn_blocking

CPU-intensive image operations (decode, Lanczos3 resize, JPEG encode)
were running directly on the async worker thread, blocking all other
requests for 100-500ms per upload. Wrap in spawn_blocking in both
upload_image() and save_deferred_image().
This commit is contained in:
Jon Seager 2026-02-10 17:11:54 +00:00
parent cf56ac28f7
commit 082b582cb3
No known key found for this signature in database

View file

@ -105,7 +105,10 @@ pub(crate) async fn upload_image(
let (upload, _source) = payload.into_parts(); let (upload, _source) = payload.into_parts();
let processed = process_data_url(&upload.image) let image_data = upload.image;
let processed = tokio::task::spawn_blocking(move || process_data_url(&image_data))
.await
.map_err(|e| AppError::unexpected(format!("image processing task failed: {e}")))?
.map_err(|e| AppError::validation(format!("invalid image: {e}")))?; .map_err(|e| AppError::validation(format!("invalid image: {e}")))?;
let image = EntityImage { let image = EntityImage {
@ -232,22 +235,27 @@ pub(crate) async fn save_deferred_image(
let Some(data_url) = data_url.filter(|s| !s.is_empty()) else { let Some(data_url) = data_url.filter(|s| !s.is_empty()) else {
return; return;
}; };
match process_data_url(data_url) { let data_url = data_url.to_string();
Ok(processed) => { let processed = match tokio::task::spawn_blocking(move || process_data_url(&data_url)).await {
let image = EntityImage { Ok(Ok(p)) => p,
entity_type: entity_type.to_string(), Ok(Err(err)) => {
entity_id, tracing::warn!(entity_type, entity_id, error = %err, "failed to process deferred image");
content_type: processed.content_type, return;
image_data: processed.image_data,
thumbnail_data: processed.thumbnail_data,
};
if let Err(err) = state.image_repo.upsert(image).await {
tracing::warn!(entity_type, entity_id, error = %err, "failed to save deferred image");
}
} }
Err(err) => { Err(err) => {
tracing::warn!(entity_type, entity_id, error = %err, "failed to process deferred image"); tracing::warn!(entity_type, entity_id, error = %err, "deferred image task panicked");
return;
} }
};
let image = EntityImage {
entity_type: entity_type.to_string(),
entity_id,
content_type: processed.content_type,
image_data: processed.image_data,
thumbnail_data: processed.thumbnail_data,
};
if let Err(err) = state.image_repo.upsert(image).await {
tracing::warn!(entity_type, entity_id, error = %err, "failed to save deferred image");
} }
} }