{"record":{"id":"c72f0df933b818ff","repo":"libnyanpasu/clash-nyanpasu","slug":"failed-to-join-notify-task-error","errorCode":null,"errorMessage":"failed to join notify task: {error}","messagePattern":"failed to join notify task: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/nyanpasu-core/src/state/transaction/notify.rs","lineNumber":192,"sourceCode":"            let subscriber = Arc::clone(subscriber);\n            join_set.spawn(async move { (index, Self::notify_one(&change, subscriber).await) });\n        }\n\n        let mut acks = Vec::new();\n        while let Some(res) = join_set.join_next().await {\n            match res {\n                Ok((index, ack)) => acks.push((index, ack)),\n                Err(error) => {\n                    tracing::error!(\"failed to join notify task: {error}\");\n                    acks.push((\n                        usize::MAX,\n                        SubscriberAck {\n                            name: SubscriberName(Cow::Borrowed(\"<notify task join failure>\")),\n                            policy: AckPolicy::Required,\n                            timeout: Duration::from_secs(0),\n                            elapsed: Duration::from_secs(0),\n                            status: AckStatus::Failed {\n                                error: anyhow::anyhow!(\"failed to join notify task: {error}\")\n                                    .into(),\n                            },\n                        },\n                    ));\n                }\n            }\n        }\n        acks.sort_by_key(|&(index, _)| index);\n        acks.into_iter().map(|(_, ack)| ack).collect()\n    }\n}\n\nimpl<T> NotifyExecutor<T, Prepared, Sequential>\nwhere\n    T: Clone + Send + Sync + 'static,\n{\n    pub async fn notify_all(\n        change: &StateChange<T>,","sourceCodeStart":174,"sourceCodeEnd":210,"githubUrl":"https://github.com/libnyanpasu/clash-nyanpasu/blob/f7dbce2997c633e484f54788035e770b3ee99773/backend/nyanpasu-core/src/state/transaction/notify.rs#L174-L210","documentation":"Raised as an AckStatus::Failed entry by the parallel notify executor when `JoinSet::join_next()` returns a JoinError — i.e. a spawned subscriber-notification task panicked or was cancelled and its result could not be joined. The library synthesizes a synthetic failed ack named \"<notify task join failure>\" so the transaction still records that a required subscriber notification did not complete, rather than silently losing the notification.","triggerScenarios":"A subscriber notification task spawned inside `NotifyExecutor::notify_all` panics or is aborted, causing `join_set.join_next()` to yield Err(error); the synthetic ack is pushed with this message.","commonSituations":"A subscriber callback panics on the new state value (e.g. unwrapping None, index panic); a notification task is aborted by runtime shutdown; a bug in a subscriber's async handler causes task cancellation.","solutions":["Find the subscriber whose task panicked: the JoinError carries the panic message; fix the panic in that subscriber's notification handler.","Harden subscriber callbacks against unexpected state values (no unwrap/expect on state payloads; validate before use).","Check for runtime shutdown/abort calls that could cancel in-flight notify tasks and ensure transactions complete before shutdown.","Treat the resulting transaction result as degraded — required acks failed — and re-notify or resubscribe the affected subscriber."],"exampleFix":"// before: panicking subscriber\nfn on_change(state: &Config) { let v = state.tun.as_ref().unwrap(); }\n// after: defensive subscriber\nfn on_change(state: &Config) {\n    let Some(v) = state.tun.as_ref() else { tracing::warn!(\"tun missing\"); return };\n}\n","handlingStrategy":"try-catch","validationCode":"// smoke-test subscribers before subscribing them to real transactions\nfn validate_subscriber<T>(sub: &ArcStateSubscriber<T>, sample: &StateChange<T>) {\n    assert!(tokio::spawn(sub.notify(sample)).await.is_ok(), \"subscriber task panicked\");\n}","typeGuard":"fn is_join_failure(ack: &SubscriberAck) -> bool {\n    matches!(&ack.status, AckStatus::Failed { .. })\n        && ack.name.0.contains(\"notify task join failure\")\n}","tryCatchPattern":"let acks = NotifyExecutor::notify_all(&change, &subs).await;\nfor ack in acks {\n    if let AckStatus::Failed { error } = &ack.status {\n        if ack.name.0.contains(\"notify task join failure\") {\n            tracing::error!(\"subscriber task panicked: {error:#}\");\n            // reschedule notification for the affected subscriber\n        }\n    }\n}","preventionTips":["Never panic in subscriber notification handlers; return Result instead of unwrap/expect.","Fuzz/panic-test subscriber callbacks with unusual state values before wiring them into transactions.","Avoid aborting notification tasks; let transactions drain before runtime shutdown.","Treat required-ack join failures as degraded transactions and re-notify rather than ignoring the ack."],"tags":["rust","tokio","task-panic","subscriber-notification","state-management"],"backgroundTag":"internal-invariant-violation","analyzedSha":"f7dbce2997c633e484f54788035e770b3ee99773","analyzedAt":"2026-09-08T01:24:59.197Z","contentChangedAt":"2026-09-08T01:24:59.197Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}