{"record":{"id":"12398bccc8a77b5c","repo":"spacedriveapp/spacedrive","slug":"worker-channel-closed-trying-to-shutdown","errorCode":null,"errorMessage":"Worker channel closed trying to shutdown","messagePattern":"Worker channel closed trying to shutdown","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/task-system/src/worker/mod.rs","lineNumber":194,"sourceCode":"\t\t\t.send(WorkerMessage::ForceAbortion { task_id, ack })\n\t\t\t.await\n\t\t\t.expect(\"Worker channel closed trying to force task abortion\");\n\t}\n\n\t#[instrument(skip(self), fields(worker_id = self.id))]\n\tpub async fn shutdown(&self) {\n\t\tif let Some(handle) = self\n\t\t\t.handle\n\t\t\t.try_borrow_mut()\n\t\t\t.ok()\n\t\t\t.and_then(|mut maybe_handle| maybe_handle.take())\n\t\t{\n\t\t\tlet (tx, rx) = oneshot::channel();\n\n\t\t\tself.msgs_tx\n\t\t\t\t.send(WorkerMessage::ShutdownRequest(tx))\n\t\t\t\t.await\n\t\t\t\t.expect(\"Worker channel closed trying to shutdown\");\n\n\t\t\trx.await.expect(\"Worker channel closed trying to shutdown\");\n\n\t\t\tif let Err(e) = handle.await {\n\t\t\t\tif e.is_panic() {\n\t\t\t\t\terror!(\"Worker {} critically failed: {e:#?}\", self.id);\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\twarn!(\"Trying to shutdown a worker that was already shutdown\");\n\t\t}\n\t}\n}\n\n/// SAFETY: Due to usage of refcell we lost `Sync` impl, but we only use it to have a shutdown method\n/// receiving `&self` which is called once, and we also use `try_borrow_mut` so we never panic\nunsafe impl<E: RunError> Sync for Worker<E> {}\n","sourceCodeStart":176,"sourceCodeEnd":212,"githubUrl":"https://github.com/spacedriveapp/spacedrive/blob/6dfeccf2113039e35f2ce735f945e70dc3e4ea45/crates/task-system/src/worker/mod.rs#L176-L212","documentation":"During Worker::shutdown the ShutdownRequest is sent over the worker's command channel and .expect() panics if the channel is already closed. The double-shutdown case is guarded (the handle is taken once; the second call only warns at worker/mod.rs:203-205), so this panic means the worker's task is gone even though the Worker object still held a JoinHandle - typically because the runtime dropped the worker task, or the worker exited without this Worker having driven shutdown.","triggerScenarios":"Calling system.shutdown() (which fans out to worker.shutdown() concurrently, system.rs:227) after the tokio runtime has started dropping tasks (e.g. shutdown invoked from synchronous Drop after runtime.drop() began, or a worker task already cancelled by runtime teardown). The send at mod.rs:191-194 then fails because msgs_rx was dropped with the worker task.","commonSituations":"Running system.shutdown() outside the async context that owns the runtime (from Drop of a wrapper struct after the runtime shut down); tests using block_on where tasks get dropped between block_on calls; production daemons where a signal handler shuts down the system after the main runtime task tree was aborted.","solutions":["Call SystemHandle::shutdown() exactly once, from inside the runtime, and await it to completion before the runtime is dropped","Library-level fix: make shutdown idempotent - if send fails with a closed channel, the worker is already down; log and proceed to await the JoinHandle instead of .expect","Check for the 'Trying to shutdown a worker that was already shutdown' warning in logs to detect double-shutdown paths in your code","Keep the Worker handle's lifetime nested inside the runtime's lifetime (don't let Worker outlive its spawning runtime)"],"exampleFix":"// before (worker/mod.rs:191-194)\nself.msgs_tx\n    .send(WorkerMessage::ShutdownRequest(tx))\n    .await\n    .expect(\"Worker channel closed trying to shutdown\");\n\n// after\nif self\n    .msgs_tx\n    .send(WorkerMessage::ShutdownRequest(tx))\n    .await\n    .is_err()\n{\n    warn!(worker_id = self.id, \"Worker channel already closed; worker is down, continuing shutdown join\");\n}","handlingStrategy":"validation","validationCode":"// Shutdown exactly once, from inside the runtime, before it is dropped.\n// The library already guards double-shutdown (warn at mod.rs:204); this panic\n// is the different case where the worker TASK is gone but the handle was not used.\n\n// correct pattern:\n// let system = TaskSystem::new(...);\n// /* ... */\n// system.handle().shutdown().await;   // inside the runtime, awaited\n// drop(system);                       // only now let the runtime end\n\n// wrong pattern (triggers this panic):\n// impl Drop for App { fn drop(&mut self) { self.rt.block_on(self.system.shutdown()); } }\n// // runtime tasks may already be dropped when block_on runs","typeGuard":null,"tryCatchPattern":"use futures::FutureExt;\nuse std::panic::AssertUnwindSafe;\n\n// Idempotent shutdown wrapper: a panic here means the worker task already\n// exited; the system is effectively down either way.\nlet _ = AssertUnwindSafe(system.handle().shutdown()).catch_unwind().await;","preventionTips":["Call system.shutdown() once from a task running on the owning runtime, and await it; never from Drop after the runtime began shutting down","Do not let Worker or SystemHandle outlive the tokio Runtime that spawned the worker tasks","In tests, prefer #[tokio::test] with shutdown awaited inside the async body over manually built runtimes dropped at scope end"],"tags":["rust","tokio","async","panic","channel","shutdown","lifetime","task-system"],"backgroundTag":null,"analyzedSha":"6dfeccf2113039e35f2ce735f945e70dc3e4ea45","analyzedAt":"2026-08-16T11:26:17.074Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}