mirror of https://github.com/hashicorp/consul
253 lines
7.0 KiB
Go
253 lines
7.0 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
// package limiter provides primatives for limiting the number of concurrent
|
|
// operations in-flight.
|
|
package limiter
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"math/rand"
|
|
"sort"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
// Unlimited can be used to allow an unlimited number of concurrent sessions.
|
|
const Unlimited uint32 = 0
|
|
|
|
// ErrCapacityReached is returned when there is no capacity for additional sessions.
|
|
var ErrCapacityReached = errors.New("active session limit reached")
|
|
|
|
// SessionLimiter is a session-based concurrency limiter, it provides the basis
|
|
// of gRPC/xDS load balancing.
|
|
//
|
|
// Stream handlers obtain a session with BeginSession before they begin serving
|
|
// resources - if the server has reached capacity ErrCapacityReached is returned,
|
|
// otherwise a Session is returned.
|
|
//
|
|
// It is the session-holder's responsibility to:
|
|
//
|
|
// 1. Call End on the session when finished.
|
|
// 2. Receive on the session's Terminated channel and exit (e.g. close the gRPC
|
|
// stream) when it is closed.
|
|
//
|
|
// The maximum number of concurrent sessions is controlled with SetMaxSessions.
|
|
// If there are more than the given maximum sessions already in-flight,
|
|
// SessionLimiter will drain randomly-selected sessions at a rate controlled
|
|
// by SetDrainRateLimit.
|
|
type SessionLimiter struct {
|
|
drainLimiter *rate.Limiter
|
|
|
|
// max and inFlight are read/written using atomic operations.
|
|
max, inFlight uint32
|
|
|
|
// wakeCh is used to trigger the Run loop to start draining excess sessions.
|
|
wakeCh chan struct{}
|
|
|
|
// Everything below here is guarded by mu.
|
|
mu sync.Mutex
|
|
maxSessionID uint64
|
|
sessionIDs []uint64 // sessionIDs must be sorted so we can binary search it.
|
|
sessions map[uint64]*session
|
|
}
|
|
|
|
// NewSessionLimiter creates a new SessionLimiter.
|
|
func NewSessionLimiter() *SessionLimiter {
|
|
return &SessionLimiter{
|
|
drainLimiter: rate.NewLimiter(rate.Inf, 1),
|
|
max: Unlimited,
|
|
wakeCh: make(chan struct{}, 1),
|
|
sessionIDs: make([]uint64, 0),
|
|
sessions: make(map[uint64]*session),
|
|
}
|
|
}
|
|
|
|
// Run the SessionLimiter's drain loop, which terminates excess sessions if the
|
|
// limit is lowered. It will exit when the given context is canceled or reaches
|
|
// its deadline.
|
|
func (l *SessionLimiter) Run(ctx context.Context) {
|
|
for {
|
|
select {
|
|
case <-l.wakeCh:
|
|
for {
|
|
if !l.overCapacity() {
|
|
break
|
|
}
|
|
|
|
if err := l.drainLimiter.Wait(ctx); err != nil {
|
|
break
|
|
}
|
|
|
|
if !l.overCapacity() {
|
|
break
|
|
}
|
|
|
|
l.terminateSession()
|
|
}
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetMaxSessions controls the maximum number of concurrent sessions. If it is
|
|
// lower, randomly-selected sessions will be drained.
|
|
func (l *SessionLimiter) SetMaxSessions(max uint32) {
|
|
atomic.StoreUint32(&l.max, max)
|
|
|
|
// Send on wakeCh without blocking if the Run loop is busy. wakeCh has a
|
|
// buffer of 1, so no triggers will be missed.
|
|
select {
|
|
case l.wakeCh <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
// SetDrainRateLimit controls the rate at which excess sessions will be drained.
|
|
func (l *SessionLimiter) SetDrainRateLimit(limit rate.Limit) {
|
|
l.drainLimiter.SetLimit(limit)
|
|
}
|
|
|
|
// BeginSession begins a new session, or returns ErrCapacityReached if the
|
|
// concurrent session limit has been reached.
|
|
//
|
|
// It is the session-holder's responsibility to:
|
|
//
|
|
// 1. Call End on the session when finished.
|
|
// 2. Receive on the session's Terminated channel and exit (e.g. close the gRPC
|
|
// stream) when it is closed.
|
|
func (l *SessionLimiter) BeginSession() (Session, error) {
|
|
if !l.hasCapacity() {
|
|
return nil, ErrCapacityReached
|
|
}
|
|
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
return l.createSessionLocked(), nil
|
|
}
|
|
|
|
// Note: hasCapacity is *best effort*. As we do not hold l.mu it's possible that:
|
|
//
|
|
// - max has changed by the time we compare it to inFlight.
|
|
// - inFlight < max now, but increases before we create a new session.
|
|
//
|
|
// This is acceptable for our uses, especially because excess sessions will
|
|
// eventually be drained.
|
|
func (l *SessionLimiter) hasCapacity() bool {
|
|
max := atomic.LoadUint32(&l.max)
|
|
if max == Unlimited {
|
|
return true
|
|
}
|
|
|
|
cur := atomic.LoadUint32(&l.inFlight)
|
|
return max > cur
|
|
}
|
|
|
|
// Note: overCapacity is *best effort*. As we do not hold l.mu it's possible that:
|
|
//
|
|
// - max has changed by the time we compare it to inFlight.
|
|
// - inFlight > max now, but decreases before we terminate a session.
|
|
func (l *SessionLimiter) overCapacity() bool {
|
|
max := atomic.LoadUint32(&l.max)
|
|
if max == Unlimited {
|
|
return false
|
|
}
|
|
|
|
cur := atomic.LoadUint32(&l.inFlight)
|
|
return cur > max
|
|
}
|
|
|
|
func (l *SessionLimiter) terminateSession() {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
idx := rand.Intn(len(l.sessionIDs))
|
|
id := l.sessionIDs[idx]
|
|
l.sessions[id].terminate()
|
|
l.deleteSessionLocked(idx, id)
|
|
}
|
|
|
|
func (l *SessionLimiter) createSessionLocked() *session {
|
|
session := &session{
|
|
l: l,
|
|
id: l.maxSessionID,
|
|
termCh: make(chan struct{}),
|
|
}
|
|
|
|
l.maxSessionID++
|
|
l.sessionIDs = append(l.sessionIDs, session.id)
|
|
l.sessions[session.id] = session
|
|
|
|
atomic.AddUint32(&l.inFlight, 1)
|
|
|
|
return session
|
|
}
|
|
|
|
func (l *SessionLimiter) deleteSessionLocked(idx int, id uint64) {
|
|
delete(l.sessions, id)
|
|
|
|
// Note: it's important that we preserve the order here (which most allocation
|
|
// free deletion tricks don't) because we binary search the slice.
|
|
l.sessionIDs = append(l.sessionIDs[:idx], l.sessionIDs[idx+1:]...)
|
|
|
|
atomic.AddUint32(&l.inFlight, ^uint32(0))
|
|
}
|
|
|
|
func (l *SessionLimiter) deleteSessionWithID(id uint64) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
idx := sort.Search(len(l.sessionIDs), func(i int) bool {
|
|
return l.sessionIDs[i] >= id
|
|
})
|
|
|
|
if idx == len(l.sessionIDs) || l.sessionIDs[idx] != id {
|
|
// It's possible that we weren't able to find the id because the session has
|
|
// already been deleted. This could be because the session-holder called End
|
|
// more than once, or because the session was drained. In either case there's
|
|
// nothing more to do.
|
|
return
|
|
}
|
|
|
|
l.deleteSessionLocked(idx, id)
|
|
}
|
|
|
|
// SessionTerminatedChan is a channel that will be closed to notify session-
|
|
// holders that a session has been terminated.
|
|
type SessionTerminatedChan <-chan struct{}
|
|
|
|
// Session allows its holder to perform an operation (e.g. serve a gRPC stream)
|
|
// concurrenly with other session-holders. Sessions may be terminated abruptly
|
|
// by the SessionLimiter, so it is the responsibility of the holder to receive
|
|
// on the Terminated channel and halt the operation when it is closed.
|
|
type Session interface {
|
|
// End the session.
|
|
//
|
|
// This MUST be called when the session-holder is done (e.g. the gRPC stream
|
|
// is closed).
|
|
End()
|
|
|
|
// Terminated is a channel that is closed when the session is terminated.
|
|
//
|
|
// The session-holder MUST receive on it and exit (e.g. close the gRPC stream)
|
|
// when it is closed.
|
|
Terminated() SessionTerminatedChan
|
|
}
|
|
|
|
type session struct {
|
|
l *SessionLimiter
|
|
|
|
id uint64
|
|
termCh chan struct{}
|
|
}
|
|
|
|
func (s *session) End() { s.l.deleteSessionWithID(s.id) }
|
|
|
|
func (s *session) Terminated() SessionTerminatedChan { return s.termCh }
|
|
|
|
func (s *session) terminate() { close(s.termCh) }
|