Zackriya-Solutions/meetily · error · anyhow::Error
Download worker ended before completing cancellation cleanup
Error message
Download worker ended before completing cancellation cleanup
What it means
cancel_download_with_timeout signals cancellation, then waits on the download worker's completion watch channel with a cleanup timeout. If the watch channel is dropped (all senders gone) before completion ever flips to true, completion.changed() yields Err(Lagged/Closed) — meaning the worker task terminated without running finish_download's cleanup (which is what sends completion=true). This is an internal invariant violation in the download lifecycle: the worker died mid-flight instead of finalizing.
Source
Thrown at frontend/src-tauri/src/whisper_engine/whisper_engine.rs:1262
async fn cancel_download_with_timeout(
&self,
model_name: &str,
cleanup_timeout: Duration,
) -> Result<CancelDownloadOutcome> {
log::info!("Cancelling download for model: {}", model_name);
let Some(active_download) = self.active_downloads.lock().await.get(model_name).cloned() else {
return Ok(CancelDownloadOutcome::Cancelled);
};
active_download.cancellation.cancel();
let mut completion = active_download.completion.subscribe();
if !*completion.borrow() {
match tokio::time::timeout(cleanup_timeout, completion.changed()).await {
Ok(Ok(())) => {}
Ok(Err(_)) => {
return Err(anyhow!("Download worker ended before completing cancellation cleanup"));
}
Err(_) => {
return Ok(CancelDownloadOutcome::Pending);
}
}
}
Ok(CancelDownloadOutcome::Cancelled)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio::sync::oneshot;
use tokio::time::{timeout, Duration};View on GitHub (pinned to a2cb62e827)
Solutions
- Find why the worker task died: enable RUST_LOG=debug and look for a panic or abort in the download task before cancellation cleanup.
- Ensure every path that ends the worker calls finish_download (which sets completion) — wrap the worker body so panics still finalize (catch_unwind or a finalizer guard).
- If the task was aborted deliberately, treat the download as cancelled on the caller side: retry cancel_download or reload model status from disk and clean up the partial file.
- Check for runtime shutdown races (app closing while download active) and either block shutdown until cleanup or tolerate Pending/worker-gone outcomes.
- As a hardening step, on this error remove the model from active_downloads and delete any partial ggml-<name>.bin so state doesn't get stuck in 'Downloading'.
Example fix
// before: worker panics abort and drop the watch sender without cleanup
let handle = tokio::spawn(engine.download_model(name, cb));
// after: guarantee finalization even on panic
let engine2 = engine.clone();
let handle = tokio::spawn(async move {
let r = engine2.download_model(name, cb).await;
r
});
// and inside the worker wrapper, ensure finish_download always runs:
let result = std::panic::AssertUnwindSafe(download_future)
.catch_unwind()
.await
.unwrap_or_else(|_| Err(anyhow!("download worker panicked")));
engine.finish_download(model_name, &active_download, &file_path, result).await Defensive patterns
Strategy: try-catch
Validate before calling
// before cancelling, confirm the worker is still alive
// (heuristic: watch channel still has senders)
if active_download.completion.borrow().is_changed().is_err() {
// channel open — cancellation should complete normally
} Type guard
fn worker_channel_alive<T>(rx: &tokio::sync::watch::Receiver<T>) -> bool {
// has_changed() errors only when the sender side was dropped
rx.has_changed().is_ok()
} Try / catch
match engine.cancel_download(model_name).await {
Ok(CancelDownloadOutcome::Cancelled) => { /* clean */ }
Ok(CancelDownloadOutcome::Pending) => { /* still cleaning; poll status */ }
Err(e) if e.to_string().contains("ended before completing cancellation cleanup") => {
// worker died: force-recover state
engine.cleanup_stuck_download(model_name).await; // remove from active_downloads, delete partial file
}
Err(e) => return Err(e),
} Prevention
- Wrap download workers so finish_download always runs — use a drop guard or catch_unwind around the future.
- Never abort download tasks; always cancel via the CancellationToken so cleanup executes.
- Treat watch-channel closure as a bug signal: log it with backtrace context and add a recovery path that clears stuck 'Downloading' state.
- On app shutdown, either await active download finalization or explicitly mark those models Missing on next startup scan.
When it happens
Trigger: The tokio task running download_model_from_url is aborted/panics before finish_download calls completion.send_replace(true), so the ActiveDownload's completion sender is dropped while completion is still false and the canceller observes channel closure instead of a completion signal.
Common situations: A panic inside the download worker (e.g. unwrap on chunk write) or task::abort of the spawned download; runtime shutdown (window closed) killing the worker task; a bug where finish_download's early-return path forgets to set completion — though the current code sets it, a future refactor could break that invariant.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- Download cancelled by user
- Download worker ended before completing cancellation cleanup
- Download cancelled by user
- Decode task join error: {}
- Resample task join error: {}
AI-assisted analysis of Zackriya-Solutions/meetily@a2cb62e827 (2026-09-12).
Data as JSON: /api/errors/e4ddd47738f7ebfa.
Report an issue: GitHub.