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 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}")))?;
let image = EntityImage {
@ -232,8 +235,18 @@ pub(crate) async fn save_deferred_image(
let Some(data_url) = data_url.filter(|s| !s.is_empty()) else {
return;
};
match process_data_url(data_url) {
Ok(processed) => {
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,
Ok(Err(err)) => {
tracing::warn!(entity_type, entity_id, error = %err, "failed to process deferred image");
return;
}
Err(err) => {
tracing::warn!(entity_type, entity_id, error = %err, "deferred image task panicked");
return;
}
};
let image = EntityImage {
entity_type: entity_type.to_string(),
entity_id,
@ -245,11 +258,6 @@ pub(crate) async fn save_deferred_image(
tracing::warn!(entity_type, entity_id, error = %err, "failed to save deferred image");
}
}
Err(err) => {
tracing::warn!(entity_type, entity_id, error = %err, "failed to process deferred image");
}
}
}
fn image_response(data: Vec<u8>, content_type: &str) -> Response {
Response::builder()