hashicorp/nomad · critical
bug: pool exhausted
Error message
bug: pool exhausted
What it means
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.
Source
Thrown at helper/users/dynamic/pool.go:148
if p.used.Size() == int((p.max-p.min)+1) {
return none, ErrPoolExhausted
}
// attempt to select a random ugid
if id := p.random(); id != none {
return id, nil
}
// slow case where we iterate each id looking for one that is not used
for id := p.min; id <= p.max; id++ {
if !p.used.Contains(id) {
p.used.Insert(id)
return id, nil
}
}
// we checked for this case up top; if we get here there is a bug
panic("bug: pool exhausted")
}
// random will attempt to select a random UGID from the pool
func (p *pool) random() UGID {
// make up to 10 attempts to find a random unused UGID
// if all 10 attempts fail, return the sentinel indicating as much
const maxAttempts = 10
size := int64(p.max - p.min)
tries := int(min(maxAttempts, size))
for attempt := 0; attempt < tries; attempt++ {
id := UGID(rand.Int63n(size)) + p.min
if p.used.Insert(id) {
return id
}
}
return none
}
View on GitHub (pinned to 482b49bf1a)
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
Defensive patterns
Strategy: try-catch
Try / catch
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) } }() Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- ErrVolumeNameExists
- ErrAllocBroadcasterClosed
- variable already holds a lock
- invalid acl policy: %v
- acquire conflict %w
AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04).
Data as JSON: /api/errors/9e7268130857a2e1.
Report an issue: GitHub.