libnyanpasu/clash-nyanpasu · error

failed to join notify task: {error}

Error message

failed to join notify task: {error}

What it means

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.

Source

Thrown at backend/nyanpasu-core/src/state/transaction/notify.rs:192

            let subscriber = Arc::clone(subscriber);
            join_set.spawn(async move { (index, Self::notify_one(&change, subscriber).await) });
        }

        let mut acks = Vec::new();
        while let Some(res) = join_set.join_next().await {
            match res {
                Ok((index, ack)) => acks.push((index, ack)),
                Err(error) => {
                    tracing::error!("failed to join notify task: {error}");
                    acks.push((
                        usize::MAX,
                        SubscriberAck {
                            name: SubscriberName(Cow::Borrowed("<notify task join failure>")),
                            policy: AckPolicy::Required,
                            timeout: Duration::from_secs(0),
                            elapsed: Duration::from_secs(0),
                            status: AckStatus::Failed {
                                error: anyhow::anyhow!("failed to join notify task: {error}")
                                    .into(),
                            },
                        },
                    ));
                }
            }
        }
        acks.sort_by_key(|&(index, _)| index);
        acks.into_iter().map(|(_, ack)| ack).collect()
    }
}

impl<T> NotifyExecutor<T, Prepared, Sequential>
where
    T: Clone + Send + Sync + 'static,
{
    pub async fn notify_all(
        change: &StateChange<T>,

View on GitHub (pinned to f7dbce2997)

Solutions

  1. Find the subscriber whose task panicked: the JoinError carries the panic message; fix the panic in that subscriber's notification handler.
  2. Harden subscriber callbacks against unexpected state values (no unwrap/expect on state payloads; validate before use).
  3. Check for runtime shutdown/abort calls that could cancel in-flight notify tasks and ensure transactions complete before shutdown.
  4. Treat the resulting transaction result as degraded — required acks failed — and re-notify or resubscribe the affected subscriber.

Example fix

// before: panicking subscriber
fn on_change(state: &Config) { let v = state.tun.as_ref().unwrap(); }
// after: defensive subscriber
fn on_change(state: &Config) {
    let Some(v) = state.tun.as_ref() else { tracing::warn!("tun missing"); return };
}
Defensive patterns

Strategy: try-catch

Validate before calling

// smoke-test subscribers before subscribing them to real transactions
fn validate_subscriber<T>(sub: &ArcStateSubscriber<T>, sample: &StateChange<T>) {
    assert!(tokio::spawn(sub.notify(sample)).await.is_ok(), "subscriber task panicked");
}

Type guard

fn is_join_failure(ack: &SubscriberAck) -> bool {
    matches!(&ack.status, AckStatus::Failed { .. })
        && ack.name.0.contains("notify task join failure")
}

Try / catch

let acks = NotifyExecutor::notify_all(&change, &subs).await;
for ack in acks {
    if let AckStatus::Failed { error } = &ack.status {
        if ack.name.0.contains("notify task join failure") {
            tracing::error!("subscriber task panicked: {error:#}");
            // reschedule notification for the affected subscriber
        }
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08). Data as JSON: /api/errors/c72f0df933b818ff. Report an issue: GitHub.