mirror of https://github.com/k3s-io/k3s
171 lines
4.4 KiB
Go
171 lines
4.4 KiB
Go
/*
|
|
Copyright 2016 The Kubernetes 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 ratelimit
|
|
|
|
import (
|
|
"math"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Bucket models a token bucket
|
|
type Bucket struct {
|
|
unitsPerNano float64
|
|
nanosPerUnit float64
|
|
capacity int64
|
|
|
|
mutex sync.Mutex
|
|
available int64
|
|
lastRefill int64
|
|
// fractionalAvailable "buffers" any amounts that flowed into the bucket smaller than one unit
|
|
// This lets us retain precision even with pathological refill rates like (1E9 + 1) per second
|
|
fractionalAvailable float64
|
|
}
|
|
|
|
// NewBucketWithRate creates a new token bucket, with maximum capacity = initial capacity, and a refill rate of qps
|
|
// We use floats for refill calculations, which introduces the possibility of truncation and rounding errors.
|
|
// For "sensible" qps values though, is is acceptable: jbeda did some tests here https://play.golang.org/p/LSKUOGz2LG
|
|
func NewBucketWithRate(qps float64, capacity int64) *Bucket {
|
|
unitsPerNano := qps / 1E9
|
|
nanosPerUnit := 1E9 / qps
|
|
b := &Bucket{
|
|
unitsPerNano: unitsPerNano,
|
|
nanosPerUnit: nanosPerUnit,
|
|
capacity: capacity,
|
|
available: capacity,
|
|
lastRefill: time.Now().UnixNano(),
|
|
}
|
|
return b
|
|
}
|
|
|
|
// Take takes n units from the bucket, reducing the available quantity even below zero,
|
|
// but then returns the amount of time we should wait
|
|
func (b *Bucket) Take(n int64) time.Duration {
|
|
b.mutex.Lock()
|
|
defer b.mutex.Unlock()
|
|
|
|
var d time.Duration
|
|
if b.available >= n {
|
|
// Fast path when bucket has sufficient availability before refilling
|
|
} else {
|
|
b.refill()
|
|
|
|
if b.available < n {
|
|
deficit := n - b.available
|
|
d = time.Duration(int64(float64(deficit) * b.nanosPerUnit))
|
|
}
|
|
}
|
|
|
|
b.available -= n
|
|
|
|
return d
|
|
}
|
|
|
|
// TakeAvailable immediately takes whatever quantity is available, up to max
|
|
func (b *Bucket) TakeAvailable(max int64) int64 {
|
|
b.mutex.Lock()
|
|
defer b.mutex.Unlock()
|
|
|
|
var took int64
|
|
if b.available >= max {
|
|
// Fast path when bucket has sufficient availability before refilling
|
|
took = max
|
|
} else {
|
|
b.refill()
|
|
|
|
took = b.available
|
|
|
|
if took < 0 {
|
|
took = 0
|
|
} else if took > max {
|
|
took = max
|
|
}
|
|
}
|
|
|
|
if took > 0 {
|
|
b.available -= took
|
|
}
|
|
|
|
return took
|
|
}
|
|
|
|
// Wait combines a call to Take with a sleep call
|
|
func (b *Bucket) Wait(n int64) {
|
|
d := b.Take(n)
|
|
if d != 0 {
|
|
time.Sleep(d)
|
|
}
|
|
}
|
|
|
|
// Capacity returns the maximum capacity of the bucket
|
|
func (b *Bucket) Capacity() int64 {
|
|
return b.capacity
|
|
}
|
|
|
|
// Available returns the quantity available in the bucket (which may be negative), but does not take it.
|
|
// This function is for diagnostic / informational purposes only - the returned capacity may immediately
|
|
// be inaccurate if another thread is operating on the bucket concurrently.
|
|
func (b *Bucket) Available() int64 {
|
|
b.mutex.Lock()
|
|
defer b.mutex.Unlock()
|
|
|
|
b.refill()
|
|
|
|
return b.available
|
|
}
|
|
|
|
// refill replenishes the bucket based on elapsed time; mutex must be held
|
|
func (b *Bucket) refill() {
|
|
// Note that we really want a monotonic clock here, but go says no:
|
|
// https://github.com/golang/go/issues/12914
|
|
now := time.Now().UnixNano()
|
|
|
|
b.refillAtTimestamp(now)
|
|
}
|
|
|
|
// refillAtTimestamp is the logic of the refill function, for testing
|
|
func (b *Bucket) refillAtTimestamp(now int64) {
|
|
nanosSinceLastRefill := now - b.lastRefill
|
|
if nanosSinceLastRefill <= 0 {
|
|
// we really want monotonic
|
|
return
|
|
}
|
|
|
|
// Compute units that have flowed into bucket
|
|
refillFloat := (float64(nanosSinceLastRefill) * b.unitsPerNano) + b.fractionalAvailable
|
|
if refillFloat > float64(b.capacity) {
|
|
// float64 > MaxInt64 can be converted to negative int64; side step this
|
|
b.available = b.capacity
|
|
|
|
// Don't worry about the fractional units with huge refill rates
|
|
} else {
|
|
whole, fraction := math.Modf(refillFloat)
|
|
refill := int64(whole)
|
|
b.fractionalAvailable = fraction
|
|
if refill != 0 {
|
|
// Refill with overflow
|
|
b.available += refill
|
|
if b.available >= b.capacity {
|
|
b.available = b.capacity
|
|
b.fractionalAvailable = 0
|
|
}
|
|
}
|
|
|
|
}
|
|
b.lastRefill = now
|
|
}
|