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

  1. Find why the worker task died: enable RUST_LOG=debug and look for a panic or abort in the download task before cancellation cleanup.
  2. 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).
  3. 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.
  4. Check for runtime shutdown races (app closing while download active) and either block shutdown until cleanup or tolerate Pending/worker-gone outcomes.
  5. 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

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


AI-assisted analysis of Zackriya-Solutions/meetily@a2cb62e827 (2026-09-12). Data as JSON: /api/errors/e4ddd47738f7ebfa. Report an issue: GitHub.