spacedriveapp/spacedrive · error

Stolen task channel closed

Error message

Stolen task channel closed

What it means

WorkStealer::steal ends by sending None ('nothing was stolen') back to the requesting worker's stole_task_tx and panics via .expect() if that channel is closed. The receiver, stole_task_rx, lives inside the requesting worker's merged message stream (run.rs:40-46); when that run future dies without aborting the detached steal task, the stream is dropped, the channel closes, and the terminal send at mod.rs:289-292 panics inside the spawned steal task.

Source

Thrown at crates/task-system/src/worker/mod.rs:292

			.skip(stealer_id)
			// Taking the total amount of workers
			.take(total_workers)
			// Removing the current worker as we can't steal from ourselves
			.filter(|worker_comm| worker_comm.worker_id != stealer_id)
		{
			if worker_comm
				.steal_task(stealer_id, stolen_task_tx.clone())
				.await
			{
				trace!(stolen_worker_id = worker_comm.worker_id, "Stole a task");
				return;
			}
		}

		stolen_task_tx
			.send(None)
			.await
			.expect("Stolen task channel closed");
	}
}

View on GitHub (pinned to 6dfeccf211)

Solutions

  1. Library-level fix: make the terminal send non-fatal - let _ = stolen_task_tx.send(None).await; - a closed receiver means the requester is gone and the None signal is moot
  2. Library-level fix: abort the outstanding steal task on every exit path from run, not just NewTask/shutdown - e.g. a guard in run() (or Drop on Runner) that calls abort_steal_task(), or move current_steal_task_handle somewhere that survives into the respawn loop
  3. Fix the underlying worker panic that triggers the restart (see the 'Worker critically failed' log) - this error is a secondary symptom
  4. Keep runtime/task lifetimes stable so run futures are never dropped mid-sweep

Example fix

// before (worker/mod.rs:289-292)
stolen_task_tx
    .send(None)
    .await
    .expect("Stolen task channel closed");

// after
if stolen_task_tx.send(None).await.is_err() {
    trace!(stealer_id, "Requesting worker gone before 'no task stolen' signal; nothing to do");
}
Defensive patterns

Strategy: fallback

Try / catch

// Panic fires in the detached WorkStealer::steal task; catch it with a hook:
std::panic::set_hook(Box::new(|info| {
    let msg = info.to_string();
    if msg.contains("Stolen task channel closed") {
        // requester's run loop died mid-sweep; the None signal is moot
        return;
    }
    eprintln!("panic: {msg}");
}));

Prevention

When it happens

Trigger: The requesting worker's run future panicking and the respawn loop restarting it (mod.rs:69-83): the old msg_stream holding stole_task_rx is dropped, but the steal task spawned at runner.rs:1402-1407 is only aborted via abort_steal_task() on the guarded paths (NewTask at run.rs:53 and shutdown at runner.rs:577) - the panic-restart path drops the Runner (which owns current_steal_task_handle) without aborting, so the detached steal task survives just long enough to panic on send(None). Also possible during runtime teardown.

Common situations: Any worker panic-restart while an idle steal sweep is in flight (steals fire every second per idle worker, runner.rs:917-929, so the overlap window is large in mostly-idle pools); production logs showing an stray panic from a detached task right after a 'Worker critically failed and will restart' line.

Related errors


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