{"record":{"id":"e4ddd47738f7ebfa","repo":"Zackriya-Solutions/meetily","slug":"download-worker-ended-before-completing-cancellation-cleanup-e4ddd4","errorCode":null,"errorMessage":"Download worker ended before completing cancellation cleanup","messagePattern":"Download worker ended before completing cancellation cleanup","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"frontend/src-tauri/src/whisper_engine/whisper_engine.rs","lineNumber":1262,"sourceCode":"    async fn cancel_download_with_timeout(\n        &self,\n        model_name: &str,\n        cleanup_timeout: Duration,\n    ) -> Result<CancelDownloadOutcome> {\n        log::info!(\"Cancelling download for model: {}\", model_name);\n\n        let Some(active_download) = self.active_downloads.lock().await.get(model_name).cloned() else {\n            return Ok(CancelDownloadOutcome::Cancelled);\n        };\n\n        active_download.cancellation.cancel();\n\n        let mut completion = active_download.completion.subscribe();\n        if !*completion.borrow() {\n            match tokio::time::timeout(cleanup_timeout, completion.changed()).await {\n                Ok(Ok(())) => {}\n                Ok(Err(_)) => {\n                    return Err(anyhow!(\"Download worker ended before completing cancellation cleanup\"));\n                }\n                Err(_) => {\n                    return Ok(CancelDownloadOutcome::Pending);\n                }\n            }\n        }\n\n        Ok(CancelDownloadOutcome::Cancelled)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use tokio::io::{AsyncReadExt, AsyncWriteExt};\n    use tokio::net::TcpListener;\n    use tokio::sync::oneshot;\n    use tokio::time::{timeout, Duration};","sourceCodeStart":1244,"sourceCodeEnd":1280,"githubUrl":"https://github.com/Zackriya-Solutions/meetily/blob/a2cb62e827da7ef59f65064c97233efb2313878e/frontend/src-tauri/src/whisper_engine/whisper_engine.rs#L1244-L1280","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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'."],"exampleFix":"// before: worker panics abort and drop the watch sender without cleanup\nlet handle = tokio::spawn(engine.download_model(name, cb));\n// after: guarantee finalization even on panic\nlet engine2 = engine.clone();\nlet handle = tokio::spawn(async move {\n    let r = engine2.download_model(name, cb).await;\n    r\n});\n// and inside the worker wrapper, ensure finish_download always runs:\nlet result = std::panic::AssertUnwindSafe(download_future)\n    .catch_unwind()\n    .await\n    .unwrap_or_else(|_| Err(anyhow!(\"download worker panicked\")));\nengine.finish_download(model_name, &active_download, &file_path, result).await","handlingStrategy":"try-catch","validationCode":"// before cancelling, confirm the worker is still alive\n// (heuristic: watch channel still has senders)\nif active_download.completion.borrow().is_changed().is_err() {\n    // channel open — cancellation should complete normally\n}","typeGuard":"fn worker_channel_alive<T>(rx: &tokio::sync::watch::Receiver<T>) -> bool {\n    // has_changed() errors only when the sender side was dropped\n    rx.has_changed().is_ok()\n}","tryCatchPattern":"match engine.cancel_download(model_name).await {\n    Ok(CancelDownloadOutcome::Cancelled) => { /* clean */ }\n    Ok(CancelDownloadOutcome::Pending) => { /* still cleaning; poll status */ }\n    Err(e) if e.to_string().contains(\"ended before completing cancellation cleanup\") => {\n        // worker died: force-recover state\n        engine.cleanup_stuck_download(model_name).await; // remove from active_downloads, delete partial file\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["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."],"tags":["async","tokio","cancellation","internal-invariant","rust"],"backgroundTag":"internal-invariant-violation","analyzedSha":"a2cb62e827da7ef59f65064c97233efb2313878e","analyzedAt":"2026-09-12T11:12:14.152Z","contentChangedAt":"2026-09-12T11:12:14.152Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}