{"record":{"id":"9e7268130857a2e1","repo":"hashicorp/nomad","slug":"bug-pool-exhausted","errorCode":null,"errorMessage":"bug: pool exhausted","messagePattern":"bug: pool exhausted","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"helper/users/dynamic/pool.go","lineNumber":148,"sourceCode":"\tif p.used.Size() == int((p.max-p.min)+1) {\n\t\treturn none, ErrPoolExhausted\n\t}\n\n\t// attempt to select a random ugid\n\tif id := p.random(); id != none {\n\t\treturn id, nil\n\t}\n\n\t// slow case where we iterate each id looking for one that is not used\n\tfor id := p.min; id <= p.max; id++ {\n\t\tif !p.used.Contains(id) {\n\t\t\tp.used.Insert(id)\n\t\t\treturn id, nil\n\t\t}\n\t}\n\n\t// we checked for this case up top; if we get here there is a bug\n\tpanic(\"bug: pool exhausted\")\n}\n\n// random will attempt to select a random UGID from the pool\nfunc (p *pool) random() UGID {\n\t// make up to 10 attempts to find a random unused UGID\n\t// if all 10 attempts fail, return the sentinel indicating as much\n\tconst maxAttempts = 10\n\tsize := int64(p.max - p.min)\n\ttries := int(min(maxAttempts, size))\n\tfor attempt := 0; attempt < tries; attempt++ {\n\t\tid := UGID(rand.Int63n(size)) + p.min\n\t\tif p.used.Insert(id) {\n\t\t\treturn id\n\t\t}\n\t}\n\treturn none\n}\n","sourceCodeStart":130,"sourceCodeEnd":166,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/helper/users/dynamic/pool.go#L130-L166","documentation":"pool.Acquire returns a free UGID by first checking whether any ID is available; if it reaches the end without finding one, it panics with 'bug: pool exhausted', because exhaustion should have been detected and returned as an error earlier. Hitting this panic indicates the pool's internal bookkeeping (used set / free count) is inconsistent.","triggerScenarios":"Calling Acquire concurrently (or after a failed release path) when all UGIDs in [min,max] are in the used set, and the pre-check that should return an ErrPoolExhausted-style error was bypassed due to a race or accounting bug.","commonSituations":"A task leaked a UGID (Acquire without a matching Release), count/used-set desynchronization under concurrency, or caller code ignoring the returned error and calling Acquire again on a full pool.","solutions":["Ensure every Acquire is paired with exactly one Release; check for leaked UGIDs in task cleanup paths","Report as a bug if reproducing with correct Acquire/Release usage — include a goroutine trace","Work around by restarting the client/agent to rebuild the pool state","Avoid hammering Acquire on a full pool; handle the exhaustion error before this panic point is reached"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"defer func() { if r := recover(); r != nil { if s, ok := r.(string); ok && strings.Contains(s, \"bug: pool exhausted\") { log.Fatalf(\"UGID pool accounting bug: %v\", s) }; panic(r) } }()","preventionTips":["Always pair Acquire with Release, including on error paths (use defer)","Watch for UGID leaks in task cleanup code","Treat this panic as a library bug and report with reproduction steps","Avoid calling Acquire after receiving an exhaustion error"],"tags":["panic","concurrency","resource-leak"],"backgroundTag":"resource-pool-exhausted","analyzedSha":"482b49bf1aec006f089bcfc7e632d8f6ac303e5e","analyzedAt":"2026-09-04T07:54:14.808Z","contentChangedAt":"2026-09-04T07:54:14.808Z","schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}