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
- Do not close the coordinator's semaphore; treat it as owned exclusively by the coordinator.
- Verify you are not holding a Coordinator built from a torn-down or dropped actor state; construct it via the composition root / builder instead.
- If this panic reproduces, file a bug with the coordinator construction and teardown sequence — it indicates an internal invariant violation.
- 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
- Never close the coordinator's internal semaphore.
- Obtain the coordinator only from the composition root / builder.
- Don't reuse coordinator handles after actor shutdown.
- Treat any occurrence as a bug report.
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
- version overflow
- prepare() called more than once with different identifiers.
- URL event received before prepare() was called
- listen() called before prepare()
- prepare() called more than once with different identifiers.
AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08).
Data as JSON: /api/errors/d6d45e989929a28b.
Report an issue: GitHub.