tonhowtf/omniget · critical
host semaphore closed unexpectedly
Error message
host semaphore closed unexpectedly
What it means
In host_limiter, acquire() takes an owned permit from a per-host tokio Semaphore. acquire_owned returns Err only when the semaphore has been closed; the code treats that as an impossible invariant and panics with "host semaphore closed unexpectedly". Closing the semaphore is a programming error somewhere in the limiter.
Solutions
- Find and remove the semaphore.close() call (or ensure it happens only when no clones remain).
- Don't evict host entries from the map while tasks can still hold Arc<Semaphore> clones; use Arc retained-per-entry or refcount guards.
- Replace .expect with graceful handling: on Closed error, re-resolve the semaphore from the map and retry the acquire.
- Ensure shutdown drains in-flight acquire() futures before closing semaphores.
Example fix
// before
let permit = semaphore
.acquire_owned()
.await
.expect("host semaphore closed unexpectedly");
// after
let permit = match semaphore.clone().acquire_owned().await {
Ok(permit) => permit,
Err(_closed) => {
// semaphore was closed; re-resolve and retry once
let fresh = state().lock().await.semaphore_for(host_key).clone();
fresh.acquire_owned().await.expect("host semaphore closed unexpectedly")
}
}; Defensive patterns
Strategy: retry
Try / catch
loop {
match semaphore.clone().acquire_owned().await {
Ok(p) => break p,
Err(_) => {
// re-resolve fresh semaphore from the map, then retry
semaphore = state().lock().await.semaphore_for(host_key).clone();
}
}
} Prevention
- Never call semaphore.close() while tasks may hold Arc clones
- Keep host semaphores alive for the app lifetime; use permits, not close, for backpressure
- Drain in-flight acquires before any shutdown path
When it happens
Trigger: acquire() is called after something called semaphore.close() on the host's Semaphore (e.g. a shutdown/cleanup path, eviction of the host entry, or a Drop impl closing the shared semaphore while a task still holds a clone).
Common situations: Host map entries evicted/replaced under lock while in-flight tasks still reference the old Arc<Semaphore>; shutdown logic closing semaphores before draining pending acquire calls; refactoring that added close() for 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
- spawn_blocking failed
- Spawn blocking failed
- Writer task panicked
- worker task panicked
- spawn_blocking failed
AI-assisted analysis of tonhowtf/omniget@8600b91f42 (2026-09-12).
Data as JSON: /api/errors/d2933d51f09d037e.
Report an issue: GitHub.
Appendix: source
Thrown at src-tauri/src/core/host_limiter.rs:127
pub struct HostLease {
_permit: OwnedSemaphorePermit,
}
pub async fn acquire(host_key: &str) -> HostLease {
let semaphore = {
let mut guard = state().lock().await;
guard
.semaphores
.entry(host_key.to_string())
.or_insert_with(|| Arc::new(Semaphore::new(limit_for_host(host_key))))
.clone()
};
let permit = semaphore
.acquire_owned()
.await
.expect("host semaphore closed unexpectedly");
let interval_ms = interval_ms_for_host(host_key);
if interval_ms > 0 {
let wait = {
let mut guard = state().lock().await;
let now = std::time::Instant::now();
let last = guard.last_dispatch.get(host_key).copied();
let wait = match last {
Some(t) => {
let elapsed = now.duration_since(t).as_millis() as u64;
interval_ms.saturating_sub(elapsed)
}
None => 0,
};
guard.last_dispatch.insert(
host_key.to_string(),
now + std::time::Duration::from_millis(wait),
);View on GitHub (pinned to 8600b91f42)