spacedriveapp/spacedrive · error

Interrupter ack channel closed

Error message

Interrupter ack channel closed

What it means

When a task finishes, Interrupter::drop calls close() (task.rs:289-312), which drains interruption requests that arrived after completion and acks each one; the .expect at task.rs:306 panics if the requester's ack receiver was already dropped. The requester is the detached future inside TaskWorktable::pause/suspend/cancel that is itself awaiting that ack. So this panic means the other half of a pause/suspend/cancel handshake was cancelled without completing: usually Tokio runtime teardown killing detached tasks mid-handshake, or a preceding panic in the chain (the drain task aborts on the first failed ack, stranding any later requests).

Source

Thrown at crates/task-system/src/task.rs:306

	pub(super) fn close(&self) {
		self.interrupt_rx.close();
		if !self.interrupt_rx.is_empty() {
			trace!("Pending interruption requests were not handled");
			spawn({
				let interrupt_rx = self.interrupt_rx.clone();

				async move {
					let mut interrupt_stream = pin!(interrupt_rx);

					while let Some(InterruptionRequest { kind, ack }) =
						interrupt_stream.next().await
					{
						trace!(
							?kind,
							"Interrupter received interruption request after task was completed"
						);
						ack.send(()).expect("Interrupter ack channel closed");
					}
				}
				.in_current_span()
			});
		}
	}
}

#[macro_export]
macro_rules! check_interruption {
	($interrupter:ident) => {
		let interrupter: &Interrupter = $interrupter;

		match interrupter.try_check_interrupt() {
			Some($crate::InterruptionKind::Cancel) => {
				::tracing::trace!("Task was canceled by the user");
				return Ok($crate::ExecStatus::Canceled);
			}

View on GitHub (pinned to 6dfeccf211)

Solutions

  1. Quiesce before teardown: let every pause/cancel/resume handshake complete before dropping the runtime
  2. Avoid requesting interruptions exactly at shutdown; cancel-on-drop wrappers firing during teardown are a classic trigger
  3. Library fix: replace ack.send(()).expect(...) with `if ack.send(()).is_err() { warn!(...) }` — the sibling path in InterrupterFuture::poll (task.rs:209-211) already warns instead of panicking
  4. When triaging, find the FIRST panic in the chain (RUST_LOG=task_system=trace); this one is usually a cascade, not the root

Example fix

// before (crates/task-system/src/task.rs, Interrupter::close drain task)
ack.send(()).expect("Interrupter ack channel closed");

// after: match the tolerant style already used in InterrupterFuture::poll (task.rs:209-211)
if ack.send(()).is_err() {
    warn!(?kind, "Interrupter ack channel closed; requester went away during teardown");
}
Defensive patterns

Strategy: validation

Validate before calling

// Let every interruption handshake finish before tearing down the runtime.
// 1. stop issuing new pause/cancel/suspend calls
shutting_down.store(true, Ordering::Release);
// 2. await tasks to a quiesced state (handles resolved or paused and stable)
let _ = join_all(live_handles).await;
// 3. only then shutdown + runtime drop, so no drain-vs-requester race remains

Try / catch

Fires in a detached drain task; call-site catch_unwind cannot see it. Use a panic hook to log it and inspect the FIRST panic in the chain — this one is almost always downstream of another failure or runtime teardown.

Prevention

When it happens

Trigger: A pause/suspend/cancel request is queued just as the running task completes, and by the time close()'s drain task acks it, the worktable-side awaiting future was dropped (runtime shutdown killed it, or it already panicked at its own 'Task failed to ack ...' expect).

Common situations: Dropping a Runtime or ending a #[tokio::test] with a pause/cancel in flight; CancelTaskOnDrop firing during teardown; cascades that start with any other channel-closed expect in the task-system.

Related errors


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