hashicorp/consul · error
semaphore: bad release
Error message
semaphore: bad release
What it means
Dynamic.Release (lib/semaphore/semaphore.go:87) panics when the semaphore has zero acquired slots (cur < 1), i.e. when Release is called more often than Acquire succeeded. This mirrors golang.org/x/sync/semaphore semantics: misuse is a programming error, so the library panics instead of silently corrupting the count. The most frequent cause is deferring Release before checking the Acquire error, so a canceled context leads to a release that was never acquired.
Source
Thrown at lib/semaphore/semaphore.go:92
default:
s.waiters.Remove(elem)
}
s.mu.Unlock()
return err
case <-ready:
return nil
}
}
// Release releases the semaphore. It will panic if release is called on an
// empty semphore.
func (s *Dynamic) Release() {
s.mu.Lock()
defer s.mu.Unlock()
if s.cur < 1 {
panic("semaphore: bad release")
}
next := s.waiters.Front()
// If there are no waiters, just decrement and we're done
if next == nil {
s.cur--
return
}
// Need to yield our slot to the next waiter.
// Remove them from the list
s.waiters.Remove(next)
// And trigger it's chan before we release the lock
close(next.Value.(chan struct{}))
// Note we _don't_ decrement inflight since the slot was yielded directly.
}
View on GitHub (pinned to 2397ff0d76)
Solutions
- Acquire first and only defer Release after the error check: 'if err := sem.Acquire(ctx); err != nil { return err }; defer sem.Release()'
- Audit code for double-Release paths (a Release in a loop body plus one in a defer)
- If pairing is hard to see locally, wrap Dynamic in a small type that hands out a release token (func()) per successful Acquire
Example fix
// before
func work(ctx context.Context) error {
defer sem.Release() // registered too early
if err := sem.Acquire(ctx); err != nil {
return err // Release still runs -> panic on empty semaphore
}
...
}
// after
func work(ctx context.Context) error {
if err := sem.Acquire(ctx); err != nil {
return err // nothing acquired, nothing to release
}
defer sem.Release() // paired with a successful Acquire
...
} Defensive patterns
Strategy: validation
Validate before calling
if err := sem.Acquire(ctx); err != nil {
return err // no slot acquired; must NOT call Release
}
defer sem.Release() // only after a successful Acquire Try / catch
// last-resort containment at a goroutine boundary; fix the pairing instead
defer func() {
if r := recover(); r != nil {
log.Error("semaphore misuse", "panic", r)
}
}() Prevention
- Always pair defer sem.Release() directly under a successful Acquire, never above the error check
- Search for '.Release()' occurrences and confirm each has exactly one guarded Acquire
- Wrap the semaphore in a helper returning a release func() so pairing is enforced by construction
When it happens
Trigger: 'defer sem.Release()' placed before sem.Acquire(ctx) returns, so Release runs even when Acquire failed with ctx.Err(); calling Release twice for one Acquire; calling Release on a fresh zero-value/newly created semaphore that nobody acquired.
Common situations: Refactors that hoist defers to the top of the function; early-return error paths added after the defer was written; multiple goroutines releasing on behalf of a single acquire; test teardown that releases unconditionally.
Related errors
- ClusterSize not set
- failed to decode request: %v
- failed to decode batch updates: %v
- all indexers must have a non-empty name
- no indexer was supplied when creating a new cache Index
AI-assisted analysis of hashicorp/consul@2397ff0d76 (2026-08-15).
Data as JSON: /api/errors/038b4a2f63fd7510.
Report an issue: GitHub.