240 lines
6.9 KiB
Go
240 lines
6.9 KiB
Go
// Copyright 2024 The Cockroach Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
// implied. See the License for the specific language governing
|
|
// permissions and limitations under the License.
|
|
|
|
package fifo
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
)
|
|
|
|
// Semaphore implements a weighted, dynamically reconfigurable semaphore which
|
|
// respects context cancellation.
|
|
//
|
|
// The semaphore implements a FIFO policy, where Acquire requests are satisfied
|
|
// in order. This policy provides fairness and prevents starvation but is
|
|
// susceptible to head-of-line blocking, where a large request that can't be
|
|
// satisfied blocks many other small requests that could be.
|
|
type Semaphore struct {
|
|
mu struct {
|
|
sync.Mutex
|
|
|
|
capacity int64
|
|
// outstanding can exceed capacity if the capacity is dynamically decreased.
|
|
outstanding int64
|
|
|
|
waiters Queue[semaWaiter]
|
|
|
|
// numCanceled is the number of waiters in the waiters queue which have been
|
|
// canceled. It is used to determine the current number of active waiters in
|
|
// the queue which is waiters.Len() minus this value.
|
|
numCanceled int
|
|
|
|
// numHadToWait accumulates the total number of Acquire requests which had
|
|
// to wait because the semaphore was exhausted.
|
|
numHadToWait int64
|
|
}
|
|
}
|
|
|
|
// NewSemaphore creates a new semaphore with the given capacity.
|
|
func NewSemaphore(capacity int64) *Semaphore {
|
|
if capacity <= 0 {
|
|
panic("invalid capacity")
|
|
}
|
|
s := &Semaphore{}
|
|
s.mu.capacity = capacity
|
|
s.mu.waiters = MakeQueue[semaWaiter](&semaQueuePool)
|
|
return s
|
|
}
|
|
|
|
var semaQueuePool = MakeQueueBackingPool[semaWaiter]()
|
|
|
|
var ErrRequestExceedsCapacity = errors.New("request exceeds semaphore capacity")
|
|
|
|
// TryAcquire attempts to acquire n units from the semaphore without waiting. On
|
|
// success, returns true and the caller must later Release the units.
|
|
func (s *Semaphore) TryAcquire(n int64) bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if s.numWaitersLocked() == 0 && s.mu.outstanding+n <= s.mu.capacity {
|
|
s.mu.outstanding += n
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Acquire n units from the semaphore, waiting if necessary.
|
|
//
|
|
// If the context is canceled while we are waiting, returns the context error.
|
|
// If n exceeds the current capacity, returns ErrRequestExceedsCapacity.
|
|
// On success, the caller must later Release the units.
|
|
func (s *Semaphore) Acquire(ctx context.Context, n int64) error {
|
|
s.mu.Lock()
|
|
|
|
// Fast path.
|
|
if s.numWaitersLocked() == 0 && s.mu.outstanding+n <= s.mu.capacity {
|
|
s.mu.outstanding += n
|
|
s.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
if n > s.mu.capacity {
|
|
s.mu.Unlock()
|
|
return ErrRequestExceedsCapacity
|
|
}
|
|
|
|
c := chanSyncPool.Get().(chan error)
|
|
defer chanSyncPool.Put(c)
|
|
w := s.mu.waiters.PushBack(semaWaiter{n: n, c: c})
|
|
s.mu.numHadToWait++
|
|
s.mu.Unlock()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
// We need to check if we raced with a channel notify (which happens under
|
|
// the lock).
|
|
select {
|
|
case err := <-c:
|
|
// We actually fulfilled or failed the request.
|
|
return err
|
|
default:
|
|
}
|
|
// Mark the request as canceled.
|
|
w.c = nil
|
|
s.mu.numCanceled++
|
|
// If we are the head of the queue, we may be able to fulfill other waiters.
|
|
s.processWaitersLocked()
|
|
return ctx.Err()
|
|
|
|
case err := <-c:
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Release n units back. These must be units that were acquired by a previous
|
|
// Acquire call. It is legal to split up or coalesce units when releasing.
|
|
func (s *Semaphore) Release(n int64) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.mu.outstanding -= n
|
|
if s.mu.outstanding < 0 {
|
|
panic("releasing more than was acquired")
|
|
}
|
|
s.processWaitersLocked()
|
|
}
|
|
|
|
// UpdateCapacity changes the capacity of the semaphore. If the new capacity is
|
|
// smaller, the already outstanding acquisitions might exceed the new capacity
|
|
// until they are released.
|
|
//
|
|
// If there are Acquire calls that are waiting which are requesting more than
|
|
// the new capacity, they will error out with ErrRequestExceedsCapacity.
|
|
func (s *Semaphore) UpdateCapacity(capacity int64) {
|
|
if capacity <= 0 {
|
|
panic("invalid capacity")
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.mu.capacity = capacity
|
|
s.processWaitersLocked()
|
|
}
|
|
|
|
// Stats returns the current state of the semaphore.
|
|
func (s *Semaphore) Stats() SemaphoreStats {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return SemaphoreStats{
|
|
Capacity: s.mu.capacity,
|
|
Outstanding: s.mu.outstanding,
|
|
NumHadToWait: s.mu.numHadToWait,
|
|
}
|
|
}
|
|
|
|
// SemaphoreStats contains information about the current state of the semaphore.
|
|
type SemaphoreStats struct {
|
|
// Capacity is the current capacity of the semaphore.
|
|
Capacity int64
|
|
// Outstanding is the number of units that have been acquired. Note that this
|
|
// can exceed Capacity if the capacity was recently decreased.
|
|
Outstanding int64
|
|
// NumHadToWait is the total number of Acquire calls (since the semaphore was
|
|
// created) that had to wait because the semaphore was exhausted. Useful for
|
|
// cumulative metrics.
|
|
NumHadToWait int64
|
|
|
|
// TODO(radu): consider keeping track of the total amount of time the
|
|
// semaphore was exhausted (i.e. there were waiters queued).
|
|
}
|
|
|
|
func (ss SemaphoreStats) String() string {
|
|
return fmt.Sprintf("capacity: %d, outstanding: %d, num-had-to-wait: %d",
|
|
ss.Capacity, ss.Outstanding, ss.NumHadToWait)
|
|
}
|
|
|
|
type semaWaiter struct {
|
|
// n is the amount that the waiter is trying to acquire.
|
|
n int64
|
|
// c is the channel on which Acquire is blocked. If the request is canceled,
|
|
// it is set to nil.
|
|
c chan error
|
|
}
|
|
|
|
// numWaitersLocked returns how many requests (that have not been canceled) are
|
|
// waiting in the queue.
|
|
func (s *Semaphore) numWaitersLocked() int {
|
|
return s.mu.waiters.Len() - s.mu.numCanceled
|
|
}
|
|
|
|
// processWaitersLocked processes and notifies as many waiters from the head of
|
|
// the queue as possible.
|
|
func (s *Semaphore) processWaitersLocked() {
|
|
for ; s.mu.waiters.Len() > 0; s.mu.waiters.PopFront() {
|
|
switch w := s.mu.waiters.PeekFront(); {
|
|
case w.c == nil:
|
|
// Request was canceled, we can just clean it up.
|
|
s.mu.numCanceled--
|
|
if invariants && s.mu.numCanceled < 0 {
|
|
panic("negative numCanceled")
|
|
}
|
|
|
|
case s.mu.outstanding+w.n <= s.mu.capacity:
|
|
// Request can be fulfilled.
|
|
s.mu.outstanding += w.n
|
|
w.c <- nil
|
|
|
|
case w.n > s.mu.capacity:
|
|
// Request must be failed. This can happen if the capacity was decreased
|
|
// while the element was queued.
|
|
w.c <- ErrRequestExceedsCapacity
|
|
|
|
default:
|
|
// Head of the queue needs to wait some more.
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// chanSyncPool is used to pool allocations of the channels used to notify
|
|
// goroutines waiting in Acquire.
|
|
var chanSyncPool = sync.Pool{
|
|
New: func() interface{} { return make(chan error, 1) },
|
|
}
|