spacedriveapp/spacedrive · error
Worker channel closed trying to shutdown
Error message
Worker channel closed trying to shutdown
What it means
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.
Source
Thrown at crates/task-system/src/worker/mod.rs:194
.send(WorkerMessage::ForceAbortion { task_id, ack })
.await
.expect("Worker channel closed trying to force task abortion");
}
#[instrument(skip(self), fields(worker_id = self.id))]
pub async fn shutdown(&self) {
if let Some(handle) = self
.handle
.try_borrow_mut()
.ok()
.and_then(|mut maybe_handle| maybe_handle.take())
{
let (tx, rx) = oneshot::channel();
self.msgs_tx
.send(WorkerMessage::ShutdownRequest(tx))
.await
.expect("Worker channel closed trying to shutdown");
rx.await.expect("Worker channel closed trying to shutdown");
if let Err(e) = handle.await {
if e.is_panic() {
error!("Worker {} critically failed: {e:#?}", self.id);
}
}
} else {
warn!("Trying to shutdown a worker that was already shutdown");
}
}
}
/// SAFETY: Due to usage of refcell we lost `Sync` impl, but we only use it to have a shutdown method
/// receiving `&self` which is called once, and we also use `try_borrow_mut` so we never panic
unsafe impl<E: RunError> Sync for Worker<E> {}
View on GitHub (pinned to 6dfeccf211)
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)
Example fix
// before (worker/mod.rs:191-194)
self.msgs_tx
.send(WorkerMessage::ShutdownRequest(tx))
.await
.expect("Worker channel closed trying to shutdown");
// after
if self
.msgs_tx
.send(WorkerMessage::ShutdownRequest(tx))
.await
.is_err()
{
warn!(worker_id = self.id, "Worker channel already closed; worker is down, continuing shutdown join");
} Defensive patterns
Strategy: validation
Validate before calling
// Shutdown exactly once, from inside the runtime, before it is dropped.
// The library already guards double-shutdown (warn at mod.rs:204); this panic
// is the different case where the worker TASK is gone but the handle was not used.
// correct pattern:
// let system = TaskSystem::new(...);
// /* ... */
// system.handle().shutdown().await; // inside the runtime, awaited
// drop(system); // only now let the runtime end
// wrong pattern (triggers this panic):
// impl Drop for App { fn drop(&mut self) { self.rt.block_on(self.system.shutdown()); } }
// // runtime tasks may already be dropped when block_on runs Try / catch
use futures::FutureExt; use std::panic::AssertUnwindSafe; // Idempotent shutdown wrapper: a panic here means the worker task already // exited; the system is effectively down either way. let _ = AssertUnwindSafe(system.handle().shutdown()).catch_unwind().await;
Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Worker channel closed trying to pause a not running task
- Worker channel closed trying to cancel a not running task
- Worker channel closed trying to force task abortion
- Worker channel closed trying to steal task
- System channel closed trying to report working
AI-assisted analysis of spacedriveapp/spacedrive@6dfeccf211 (2026-08-16).
Data as JSON: /api/errors/12398bccc8a77b5c.
Report an issue: GitHub.