perf(send): write the fallback temp file on a blocking thread

download_to_temp wrote the whole buffered body — up to MAX_MEDIA_UPLOAD_BYTES, 50 MiB — with a sync write_all on whatever executor thread ran the prep task; six PREP slots could stall six runtime threads at once on a slow volume (Docker volume, SD card), pausing unrelated handlers and chat-action refreshes with them. The write now runs in spawn_blocking, the pattern the photo decode in the same file already uses; the failure classes are unchanged (write error = retryable resource exhaustion, panic = permanent, worker bugs must be loud).
This commit is contained in:
2026-09-24 03:30:30 +08:00
parent 67b6bd3038
commit e9dd3aece2
+14 -3
View File
@@ -123,14 +123,25 @@ async fn download_to_temp(
.map_err(|e| FallbackError::Permanent {
message: format!("temp file failed: {e}"),
})?;
use std::io::Write;
// A write failure is resource exhaustion far more often than a broken temp
// The write runs on a blocking thread: up to 50 MiB of sync disk I/O on
// an executor thread would stall whatever else that worker runs (six prep
// tasks could stall six threads at once on a slow volume). A write
// failure is resource exhaustion far more often than a broken temp
// dir (ENOSPC / EDQUOT), and that clears on its own — worth an attempt
// instead of dropping the post on the first try. Creating the file (above)
// stays permanent: a temp dir that cannot be created at all is a
// deployment fault that should fail loudly and immediately. `Retryable`
// carries no message, so the cause is logged here.
file.as_file_mut().write_all(&bytes).map_err(|e| {
let (written, file, bytes) = tokio::task::spawn_blocking(move || {
use std::io::Write;
let written = file.as_file_mut().write_all(&bytes);
(written, file, bytes)
})
.await
.map_err(|e| FallbackError::Permanent {
message: format!("upload write worker panicked: {e}"),
})?;
written.map_err(|e| {
log::error!("temp file write failed: {e}");
FallbackError::Retryable {
delay_seconds: retry_delay_seconds(0),