{"record":{"id":"2162e932be2a500a","repo":"spacedriveapp/spacedrive","slug":"interrupter-ack-channel-closed","errorCode":null,"errorMessage":"Interrupter ack channel closed","messagePattern":"Interrupter ack channel closed","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/task-system/src/task.rs","lineNumber":306,"sourceCode":"\n\tpub(super) fn close(&self) {\n\t\tself.interrupt_rx.close();\n\t\tif !self.interrupt_rx.is_empty() {\n\t\t\ttrace!(\"Pending interruption requests were not handled\");\n\t\t\tspawn({\n\t\t\t\tlet interrupt_rx = self.interrupt_rx.clone();\n\n\t\t\t\tasync move {\n\t\t\t\t\tlet mut interrupt_stream = pin!(interrupt_rx);\n\n\t\t\t\t\twhile let Some(InterruptionRequest { kind, ack }) =\n\t\t\t\t\t\tinterrupt_stream.next().await\n\t\t\t\t\t{\n\t\t\t\t\t\ttrace!(\n\t\t\t\t\t\t\t?kind,\n\t\t\t\t\t\t\t\"Interrupter received interruption request after task was completed\"\n\t\t\t\t\t\t);\n\t\t\t\t\t\tack.send(()).expect(\"Interrupter ack channel closed\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t.in_current_span()\n\t\t\t});\n\t\t}\n\t}\n}\n\n#[macro_export]\nmacro_rules! check_interruption {\n\t($interrupter:ident) => {\n\t\tlet interrupter: &Interrupter = $interrupter;\n\n\t\tmatch interrupter.try_check_interrupt() {\n\t\t\tSome($crate::InterruptionKind::Cancel) => {\n\t\t\t\t::tracing::trace!(\"Task was canceled by the user\");\n\t\t\t\treturn Ok($crate::ExecStatus::Canceled);\n\t\t\t}","sourceCodeStart":288,"sourceCodeEnd":324,"githubUrl":"https://github.com/spacedriveapp/spacedrive/blob/6dfeccf2113039e35f2ce735f945e70dc3e4ea45/crates/task-system/src/task.rs#L288-L324","documentation":"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).","triggerScenarios":"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).","commonSituations":"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.","solutions":["Quiesce before teardown: let every pause/cancel/resume handshake complete before dropping the runtime","Avoid requesting interruptions exactly at shutdown; cancel-on-drop wrappers firing during teardown are a classic trigger","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","When triaging, find the FIRST panic in the chain (RUST_LOG=task_system=trace); this one is usually a cascade, not the root"],"exampleFix":"// before (crates/task-system/src/task.rs, Interrupter::close drain task)\nack.send(()).expect(\"Interrupter ack channel closed\");\n\n// after: match the tolerant style already used in InterrupterFuture::poll (task.rs:209-211)\nif ack.send(()).is_err() {\n    warn!(?kind, \"Interrupter ack channel closed; requester went away during teardown\");\n}","handlingStrategy":"validation","validationCode":"// Let every interruption handshake finish before tearing down the runtime.\n// 1. stop issuing new pause/cancel/suspend calls\nshutting_down.store(true, Ordering::Release);\n// 2. await tasks to a quiesced state (handles resolved or paused and stable)\nlet _ = join_all(live_handles).await;\n// 3. only then shutdown + runtime drop, so no drain-vs-requester race remains","typeGuard":null,"tryCatchPattern":"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.","preventionTips":["Do not end a #[tokio::test] or drop a Runtime with pause/cancel requests in flight","Avoid pause/cancel calls racing task completion at shutdown","If maintaining a fork: make the drain ack tolerant (warn) like InterrupterFuture::poll already is"],"tags":["rust","tokio","task-system","panic","oneshot","ack-handshake","shutdown","cancellation"],"backgroundTag":null,"analyzedSha":"6dfeccf2113039e35f2ce735f945e70dc3e4ea45","analyzedAt":"2026-08-16T11:26:17.074Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}