{"record":{"id":"1a9bffd6975e11e6","repo":"aaif-goose/goose","slug":"download-is-being-cancelled-wait-for-it-to-finish","errorCode":null,"errorMessage":"Download is being cancelled; wait for it to finish before restarting","messagePattern":"Download is being cancelled; wait for it to finish before restarting","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/goose-download-manager/src/lib.rs","lineNumber":232,"sourceCode":"        model_id: String,\n        files: Vec<(String, PathBuf)>,\n        total_size_hint: u64,\n        bearer_token: Option<String>,\n        on_complete: Option<Box<dyn FnOnce() + Send + 'static>>,\n    ) -> Result<()> {\n        info!(model_id = %model_id, file_count = files.len(), \"Starting model download\");\n        {\n            let mut downloads = self\n                .downloads\n                .lock()\n                .map_err(|_| anyhow::anyhow!(\"Failed to acquire lock\"))?;\n\n            if let Some(existing) = downloads.get(&model_id) {\n                if existing.status == DownloadStatus::Downloading {\n                    anyhow::bail!(\"Download already in progress\");\n                }\n                if existing.status == DownloadStatus::Cancelled && !existing.task_exited {\n                    anyhow::bail!(\n                        \"Download is being cancelled; wait for it to finish before restarting\"\n                    );\n                }\n            }\n\n            downloads.insert(\n                model_id.clone(),\n                DownloadProgress {\n                    model_id: model_id.clone(),\n                    status: DownloadStatus::Downloading,\n                    bytes_downloaded: 0,\n                    total_bytes: total_size_hint,\n                    progress_percent: 0.0,\n                    speed_bps: None,\n                    eta_seconds: None,\n                    error: None,\n                    task_exited: false,\n                },","sourceCodeStart":214,"sourceCodeEnd":250,"githubUrl":"https://github.com/aaif-goose/goose/blob/3810898a7447ec3299be72e223d3570a7aabf0ab/crates/goose-download-manager/src/lib.rs#L214-L250","documentation":"Thrown by download_model_sharded_with_bearer_token (and its download_model / download_model_sharded wrappers) when the shared progress map already holds an entry for the same model_id whose status is Cancelled while task_exited is still false. cancel_download() only flips the status flag; the spawned download task notices it at its next checkpoint (between shards, before each HTTP request, every <=500ms of backoff sleep, or after each streamed chunk), deletes .partial files, and only then sets task_exited = true. This error is the guard against racing a restart against an in-flight cancellation.","triggerScenarios":"Calling cancel_download(model_id) and immediately calling download_model / download_model_sharded[_with_bearer_token] again with the same model_id before the background task has observed the cancel and set task_exited = true. The window is widest for multi-shard GGUF downloads where checkpoints are far apart.","commonSituations":"A UI 'Cancel' button handler that auto-restarts the download in the same tick; a retry loop that treats the Cancelled status as restartable without waiting; test code that cancels and restarts back-to-back.","solutions":["Poll get_progress(model_id) until task_exited == true (or the entry leaves the Cancelled state) before calling download again","Drive the restart off the progress state instead of a fixed sleep after cancel_download","Only start a new download under a different model_id if you genuinely want an independent concurrent download"],"exampleFix":"// before\nmanager.cancel_download(model_id)?;\nmanager.download_model(model_id.into(), url, dest, None).await?;\n\n// after: wait for the cancelled task to settle\nmanager.cancel_download(model_id)?;\nwhile let Some(p) = manager.get_progress(model_id) {\n    if p.task_exited { break; }\n    tokio::time::sleep(std::time::Duration::from_millis(100)).await;\n}\nmanager.download_model(model_id.into(), url, dest, None).await?;","handlingStrategy":"validation","validationCode":"// check the progress map before restarting a cancelled download\nfn can_restart(manager: &DownloadManager, model_id: &str) -> bool {\n    match manager.get_progress(model_id) {\n        None => true,\n        Some(p) => p.task_exited && p.status != DownloadStatus::Downloading,\n    }\n}","typeGuard":null,"tryCatchPattern":"match manager.download_model_sharded(...).await {\n    Err(e) if e.to_string().contains(\"wait for it to finish\") => {\n        // poll get_progress until task_exited, then retry the call once\n    }\n    other => other,\n}","preventionTips":["Never restart in the same handler that called cancel_download; wait for task_exited","Model UI state on the progress map (status + task_exited), not on local assumptions","Log model_id on every cancel/restart to catch racing callers"],"tags":["rust","download","concurrency","cancellation","state-machine"],"backgroundTag":null,"analyzedSha":"3810898a7447ec3299be72e223d3570a7aabf0ab","analyzedAt":"2026-08-16T10:14:26.282Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}