{"record":{"id":"891b71fdad2df88f","repo":"jdx/mise","slug":"semaphore-closed","errorCode":null,"errorMessage":"semaphore closed","messagePattern":"semaphore closed","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/backend/go.rs","lineNumber":751,"sourceCode":"}\n\nasync fn fetch_proxy_version_infos(\n    proxies: &[GoProxy],\n    path: &str,\n    versions: &[String],\n) -> Vec<VersionInfo> {\n    let encoded = Arc::new(encode_module_path(path));\n    let proxies = Arc::new(proxies.to_vec());\n    let sem = Arc::new(Semaphore::new(GO_PROXY_VERSION_INFO_CONCURRENCY));\n    let mut join_set = tokio::task::JoinSet::new();\n\n    for version in versions {\n        let proxies = proxies.clone();\n        let encoded = encoded.clone();\n        let sem = sem.clone();\n        let version = version.clone();\n        join_set.spawn(async move {\n            let _permit = sem.acquire_owned().await.expect(\"semaphore closed\");\n            let endpoint = format!(\"{encoded}/@v/{version}.info\");\n            let info = query_proxy_version_metadata(proxies.as_slice(), &endpoint).await;\n            (version, info)\n        });\n    }\n\n    let mut times = BTreeMap::new();\n    while let Some(result) = join_set.join_next().await {\n        match result {\n            Ok((version, ProxyVersionInfoResult::Found(info))) => {\n                times.insert(version, info.time);\n            }\n            Ok((version, ProxyVersionInfoResult::NotFound | ProxyVersionInfoResult::Error)) => {\n                times.insert(version, None);\n            }\n            Err(e) => warn!(\"proxy version info task panicked: {e}\"),\n        }\n    }","sourceCodeStart":733,"sourceCodeEnd":769,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/backend/go.rs#L733-L769","documentation":"The Go proxy version-info fetch spawns a task per version, each awaiting `sem.acquire_owned()`. `acquire_owned` only fails with `acquire closed`, meaning the semaphore was dropped while permits were still being awaited. The panic indicates the shared semaphore's lifetime ended before the spawned tasks finished — an internal concurrency bug.","triggerScenarios":"`fetch_proxy_version_infos` (called by `fetch_proxy_versions` when listing Go module proxy versions) with a `sem` that is dropped or closed before `join_set` tasks complete — e.g. a refactor moves `sem` into a shorter-lived scope, or cancellation drops the semaphore early.","commonSituations":"Not reachable through user configuration; appears only with concurrency lifecycle bugs in the Go backend's parallel proxy queries, possibly under cancellation/timeouts during `mise install go` version listing.","solutions":["Update mise to the latest version where the semaphore lifetime is correct","Retry the command — a transient cancellation path may have triggered it","File a bug with the backtrace if reproducible"],"exampleFix":"// before\nlet sem = Arc::new(Semaphore::new(limit)); // dropped before tasks run\n// after\nlet sem = Arc::new(Semaphore::new(limit));\nlet join_set = { /* spawn all tasks holding sem.clone() */ };\nwhile let Some(res) = join_set.join_next().await { /* ... */ } // sem outlives tasks","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"mise ls-remote go || { echo 'transient proxy listing failure — retry'; mise ls-remote go; }","preventionTips":["Keep mise updated — semaphore lifecycle bugs are fixed upstream","Retry transient failures during parallel version listing","Report reproducible cases with backtrace; this is an internal concurrency invariant"],"tags":["go","async","semaphore","tokio","panic"],"backgroundTag":"internal-invariant-violation","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}