libnyanpasu/clash-nyanpasu · critical

semaphore should never closed

Error message

semaphore should never closed

What it means

Coordinator::upsert acquires an owned permit from an internal semaphore before building new state. The semaphore is created with enough permits and is never closed by the coordinator, so acquire can only fail if the semaphore was closed — an impossible condition by design. The code uses .expect to panic loudly if that internal invariant is ever broken.

Source

Thrown at backend/nyanpasu-core/src/state/coordinator.rs:111

        let actual = self.snapshot_versioned().version;
        self.next_change_id = StateChangeId(actual.next());
        actual
    }

    fn clone_subscribers(&self) -> Subscribers<T> {
        self.subscribers.values().cloned().collect()
    }

    pub async fn upsert(
        &mut self,
        builder: impl StateAsyncBuilder<State = T>,
    ) -> Result<PrepareReport, StateChangedError> {
        let permit = self
            .semaphore
            .clone()
            .acquire_owned()
            .await
            .expect("semaphore should never closed");
        let subscribers = self.clone_subscribers();
        let notify_strategy = self.notify_strategy;
        let new_state = builder
            .build()
            .await
            .map_err(StateChangedError::Validation)?;
        let next_changed_id = self.pending_change_id();
        let current_state = self.snapshot_versioned();
        let change = StateChange {
            id: next_changed_id,
            previous: Some(current_state.clone()),
            current: Arc::new(new_state),
        };
        let tx = new_transaction(
            change,
            self.current_state.clone(),
            subscribers,
            notify_strategy,

View on GitHub (pinned to f7dbce2997)

Solutions

  1. Do not close the coordinator's semaphore; treat it as owned exclusively by the coordinator.
  2. Verify you are not holding a Coordinator built from a torn-down or dropped actor state; construct it via the composition root / builder instead.
  3. If this panic reproduces, file a bug with the coordinator construction and teardown sequence — it indicates an internal invariant violation.
  4. As a temporary guard, wrap upsert calls so a panic is caught and the coordinator is rebuilt from scratch.

Example fix

// before: reusing a coordinator after closing its semaphore
semaphore.close();
coordinator.upsert(builder).await?;

// after: never close the semaphore; use a fresh coordinator
let coordinator = StateCoordinator::new(deps); // semaphore created internally
coordinator.upsert(builder).await?;
Defensive patterns

Strategy: try-catch

Validate before calling

// Not preventable by pre-validation; ensure coordinator is alive.
assert!(!coordinator_is_shutdown(), "coordinator already torn down");

Try / catch

match std::panic::catch_unwind(AssertUnwindSafe(|| coordinator.upsert(builder))) { ... }

Prevention

When it happens

Trigger: Calling coordinator.upsert(...) after the coordinator's internal semaphore has been closed (only possible via misuse, unsafe introspection, or a bug in coordinator lifecycle/teardown).

Common situations: Practically never hit in production; seen only when custom code closes/leaks the coordinator's semaphore, or after a partially-torn-down coordinator is reused (e.g. in tests dropping and rebuilding the actor graph incorrectly).

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/d6d45e989929a28b. Report an issue: GitHub.