{"record":{"id":"d2933d51f09d037e","repo":"tonhowtf/omniget","slug":"host-semaphore-closed-unexpectedly","errorCode":null,"errorMessage":"host semaphore closed unexpectedly","messagePattern":"host semaphore closed unexpectedly","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"src-tauri/src/core/host_limiter.rs","lineNumber":127,"sourceCode":"\npub struct HostLease {\n    _permit: OwnedSemaphorePermit,\n}\n\npub async fn acquire(host_key: &str) -> HostLease {\n    let semaphore = {\n        let mut guard = state().lock().await;\n        guard\n            .semaphores\n            .entry(host_key.to_string())\n            .or_insert_with(|| Arc::new(Semaphore::new(limit_for_host(host_key))))\n            .clone()\n    };\n\n    let permit = semaphore\n        .acquire_owned()\n        .await\n        .expect(\"host semaphore closed unexpectedly\");\n\n    let interval_ms = interval_ms_for_host(host_key);\n    if interval_ms > 0 {\n        let wait = {\n            let mut guard = state().lock().await;\n            let now = std::time::Instant::now();\n            let last = guard.last_dispatch.get(host_key).copied();\n            let wait = match last {\n                Some(t) => {\n                    let elapsed = now.duration_since(t).as_millis() as u64;\n                    interval_ms.saturating_sub(elapsed)\n                }\n                None => 0,\n            };\n            guard.last_dispatch.insert(\n                host_key.to_string(),\n                now + std::time::Duration::from_millis(wait),\n            );","sourceCodeStart":109,"sourceCodeEnd":145,"githubUrl":"https://github.com/tonhowtf/omniget/blob/8600b91f4246848bac346874daa9e61c1fc5677a/src-tauri/src/core/host_limiter.rs#L109-L145","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nlet permit = semaphore\n    .acquire_owned()\n    .await\n    .expect(\"host semaphore closed unexpectedly\");\n\n// after\nlet permit = match semaphore.clone().acquire_owned().await {\n    Ok(permit) => permit,\n    Err(_closed) => {\n        // semaphore was closed; re-resolve and retry once\n        let fresh = state().lock().await.semaphore_for(host_key).clone();\n        fresh.acquire_owned().await.expect(\"host semaphore closed unexpectedly\")\n    }\n};","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"loop {\n    match semaphore.clone().acquire_owned().await {\n        Ok(p) => break p,\n        Err(_) => {\n            // re-resolve fresh semaphore from the map, then retry\n            semaphore = state().lock().await.semaphore_for(host_key).clone();\n        }\n    }\n}","preventionTips":["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"],"tags":["concurrency","tokio","semaphore","panic"],"backgroundTag":"internal-invariant-violation","analyzedSha":"8600b91f4246848bac346874daa9e61c1fc5677a","analyzedAt":"2026-09-12T14:29:19.317Z","contentChangedAt":"2026-09-12T14:29:19.317Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}