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

  1. Call SystemHandle::shutdown() exactly once, from inside the runtime, and await it to completion before the runtime is dropped
  2. 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
  3. Check for the 'Trying to shutdown a worker that was already shutdown' warning in logs to detect double-shutdown paths in your code
  4. 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

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


AI-assisted analysis of spacedriveapp/spacedrive@6dfeccf211 (2026-08-16). Data as JSON: /api/errors/12398bccc8a77b5c. Report an issue: GitHub.