diff --git a/go.mod b/go.mod index 2a630ff38..8b40ccd39 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/sirupsen/logrus v1.9.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 - go.uber.org/ratelimit v0.1.0 + go.uber.org/ratelimit v0.3.1 golang.org/x/sys v0.30.0 golang.org/x/time v0.7.0 google.golang.org/grpc v1.65.0 @@ -49,6 +49,7 @@ require ( github.com/alibabacloud-go/openapi-util v0.1.1 // indirect github.com/alibabacloud-go/tea-utils v1.4.3 // indirect github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect + github.com/benbjohnson/clock v1.3.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/go.sum b/go.sum index c4f4045af..e796d37e0 100644 --- a/go.sum +++ b/go.sum @@ -78,6 +78,8 @@ github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTs github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM= github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk= github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -362,8 +364,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/ratelimit v0.1.0 h1:U2AruXqeTb4Eh9sYQSTrMhH8Cb7M0Ian2ibBOnBcnAw= -go.uber.org/ratelimit v0.1.0/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y= +go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0= +go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/vendor/github.com/benbjohnson/clock/LICENSE b/vendor/github.com/benbjohnson/clock/LICENSE new file mode 100644 index 000000000..ce212cb1c --- /dev/null +++ b/vendor/github.com/benbjohnson/clock/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Ben Johnson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/benbjohnson/clock/README.md b/vendor/github.com/benbjohnson/clock/README.md new file mode 100644 index 000000000..4f1f82fc6 --- /dev/null +++ b/vendor/github.com/benbjohnson/clock/README.md @@ -0,0 +1,105 @@ +clock +===== + +[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/mod/github.com/benbjohnson/clock) + +Clock is a small library for mocking time in Go. It provides an interface +around the standard library's [`time`][time] package so that the application +can use the realtime clock while tests can use the mock clock. + +The module is currently maintained by @djmitche. + +[time]: https://pkg.go.dev/github.com/benbjohnson/clock + +## Usage + +### Realtime Clock + +Your application can maintain a `Clock` variable that will allow realtime and +mock clocks to be interchangeable. For example, if you had an `Application` type: + +```go +import "github.com/benbjohnson/clock" + +type Application struct { + Clock clock.Clock +} +``` + +You could initialize it to use the realtime clock like this: + +```go +var app Application +app.Clock = clock.New() +... +``` + +Then all timers and time-related functionality should be performed from the +`Clock` variable. + + +### Mocking time + +In your tests, you will want to use a `Mock` clock: + +```go +import ( + "testing" + + "github.com/benbjohnson/clock" +) + +func TestApplication_DoSomething(t *testing.T) { + mock := clock.NewMock() + app := Application{Clock: mock} + ... +} +``` + +Now that you've initialized your application to use the mock clock, you can +adjust the time programmatically. The mock clock always starts from the Unix +epoch (midnight UTC on Jan 1, 1970). + + +### Controlling time + +The mock clock provides the same functions that the standard library's `time` +package provides. For example, to find the current time, you use the `Now()` +function: + +```go +mock := clock.NewMock() + +// Find the current time. +mock.Now().UTC() // 1970-01-01 00:00:00 +0000 UTC + +// Move the clock forward. +mock.Add(2 * time.Hour) + +// Check the time again. It's 2 hours later! +mock.Now().UTC() // 1970-01-01 02:00:00 +0000 UTC +``` + +Timers and Tickers are also controlled by this same mock clock. They will only +execute when the clock is moved forward: + +```go +mock := clock.NewMock() +count := 0 + +// Kick off a timer to increment every 1 mock second. +go func() { + ticker := mock.Ticker(1 * time.Second) + for { + <-ticker.C + count++ + } +}() +runtime.Gosched() + +// Move the clock forward 10 seconds. +mock.Add(10 * time.Second) + +// This prints 10. +fmt.Println(count) +``` diff --git a/vendor/github.com/benbjohnson/clock/clock.go b/vendor/github.com/benbjohnson/clock/clock.go new file mode 100644 index 000000000..40555b303 --- /dev/null +++ b/vendor/github.com/benbjohnson/clock/clock.go @@ -0,0 +1,378 @@ +package clock + +import ( + "context" + "sort" + "sync" + "time" +) + +// Re-export of time.Duration +type Duration = time.Duration + +// Clock represents an interface to the functions in the standard library time +// package. Two implementations are available in the clock package. The first +// is a real-time clock which simply wraps the time package's functions. The +// second is a mock clock which will only change when +// programmatically adjusted. +type Clock interface { + After(d time.Duration) <-chan time.Time + AfterFunc(d time.Duration, f func()) *Timer + Now() time.Time + Since(t time.Time) time.Duration + Until(t time.Time) time.Duration + Sleep(d time.Duration) + Tick(d time.Duration) <-chan time.Time + Ticker(d time.Duration) *Ticker + Timer(d time.Duration) *Timer + WithDeadline(parent context.Context, d time.Time) (context.Context, context.CancelFunc) + WithTimeout(parent context.Context, t time.Duration) (context.Context, context.CancelFunc) +} + +// New returns an instance of a real-time clock. +func New() Clock { + return &clock{} +} + +// clock implements a real-time clock by simply wrapping the time package functions. +type clock struct{} + +func (c *clock) After(d time.Duration) <-chan time.Time { return time.After(d) } + +func (c *clock) AfterFunc(d time.Duration, f func()) *Timer { + return &Timer{timer: time.AfterFunc(d, f)} +} + +func (c *clock) Now() time.Time { return time.Now() } + +func (c *clock) Since(t time.Time) time.Duration { return time.Since(t) } + +func (c *clock) Until(t time.Time) time.Duration { return time.Until(t) } + +func (c *clock) Sleep(d time.Duration) { time.Sleep(d) } + +func (c *clock) Tick(d time.Duration) <-chan time.Time { return time.Tick(d) } + +func (c *clock) Ticker(d time.Duration) *Ticker { + t := time.NewTicker(d) + return &Ticker{C: t.C, ticker: t} +} + +func (c *clock) Timer(d time.Duration) *Timer { + t := time.NewTimer(d) + return &Timer{C: t.C, timer: t} +} + +func (c *clock) WithDeadline(parent context.Context, d time.Time) (context.Context, context.CancelFunc) { + return context.WithDeadline(parent, d) +} + +func (c *clock) WithTimeout(parent context.Context, t time.Duration) (context.Context, context.CancelFunc) { + return context.WithTimeout(parent, t) +} + +// Mock represents a mock clock that only moves forward programmically. +// It can be preferable to a real-time clock when testing time-based functionality. +type Mock struct { + mu sync.Mutex + now time.Time // current time + timers clockTimers // tickers & timers +} + +// NewMock returns an instance of a mock clock. +// The current time of the mock clock on initialization is the Unix epoch. +func NewMock() *Mock { + return &Mock{now: time.Unix(0, 0)} +} + +// Add moves the current time of the mock clock forward by the specified duration. +// This should only be called from a single goroutine at a time. +func (m *Mock) Add(d time.Duration) { + // Calculate the final current time. + t := m.now.Add(d) + + // Continue to execute timers until there are no more before the new time. + for { + if !m.runNextTimer(t) { + break + } + } + + // Ensure that we end with the new time. + m.mu.Lock() + m.now = t + m.mu.Unlock() + + // Give a small buffer to make sure that other goroutines get handled. + gosched() +} + +// Set sets the current time of the mock clock to a specific one. +// This should only be called from a single goroutine at a time. +func (m *Mock) Set(t time.Time) { + // Continue to execute timers until there are no more before the new time. + for { + if !m.runNextTimer(t) { + break + } + } + + // Ensure that we end with the new time. + m.mu.Lock() + m.now = t + m.mu.Unlock() + + // Give a small buffer to make sure that other goroutines get handled. + gosched() +} + +// runNextTimer executes the next timer in chronological order and moves the +// current time to the timer's next tick time. The next time is not executed if +// its next time is after the max time. Returns true if a timer was executed. +func (m *Mock) runNextTimer(max time.Time) bool { + m.mu.Lock() + + // Sort timers by time. + sort.Sort(m.timers) + + // If we have no more timers then exit. + if len(m.timers) == 0 { + m.mu.Unlock() + return false + } + + // Retrieve next timer. Exit if next tick is after new time. + t := m.timers[0] + if t.Next().After(max) { + m.mu.Unlock() + return false + } + + // Move "now" forward and unlock clock. + m.now = t.Next() + m.mu.Unlock() + + // Execute timer. + t.Tick(m.now) + return true +} + +// After waits for the duration to elapse and then sends the current time on the returned channel. +func (m *Mock) After(d time.Duration) <-chan time.Time { + return m.Timer(d).C +} + +// AfterFunc waits for the duration to elapse and then executes a function. +// A Timer is returned that can be stopped. +func (m *Mock) AfterFunc(d time.Duration, f func()) *Timer { + t := m.Timer(d) + t.C = nil + t.fn = f + return t +} + +// Now returns the current wall time on the mock clock. +func (m *Mock) Now() time.Time { + m.mu.Lock() + defer m.mu.Unlock() + return m.now +} + +// Since returns time since `t` using the mock clock's wall time. +func (m *Mock) Since(t time.Time) time.Duration { + return m.Now().Sub(t) +} + +// Until returns time until `t` using the mock clock's wall time. +func (m *Mock) Until(t time.Time) time.Duration { + return t.Sub(m.Now()) +} + +// Sleep pauses the goroutine for the given duration on the mock clock. +// The clock must be moved forward in a separate goroutine. +func (m *Mock) Sleep(d time.Duration) { + <-m.After(d) +} + +// Tick is a convenience function for Ticker(). +// It will return a ticker channel that cannot be stopped. +func (m *Mock) Tick(d time.Duration) <-chan time.Time { + return m.Ticker(d).C +} + +// Ticker creates a new instance of Ticker. +func (m *Mock) Ticker(d time.Duration) *Ticker { + m.mu.Lock() + defer m.mu.Unlock() + ch := make(chan time.Time, 1) + t := &Ticker{ + C: ch, + c: ch, + mock: m, + d: d, + next: m.now.Add(d), + } + m.timers = append(m.timers, (*internalTicker)(t)) + return t +} + +// Timer creates a new instance of Timer. +func (m *Mock) Timer(d time.Duration) *Timer { + m.mu.Lock() + defer m.mu.Unlock() + ch := make(chan time.Time, 1) + t := &Timer{ + C: ch, + c: ch, + mock: m, + next: m.now.Add(d), + stopped: false, + } + m.timers = append(m.timers, (*internalTimer)(t)) + return t +} + +func (m *Mock) removeClockTimer(t clockTimer) { + for i, timer := range m.timers { + if timer == t { + copy(m.timers[i:], m.timers[i+1:]) + m.timers[len(m.timers)-1] = nil + m.timers = m.timers[:len(m.timers)-1] + break + } + } + sort.Sort(m.timers) +} + +// clockTimer represents an object with an associated start time. +type clockTimer interface { + Next() time.Time + Tick(time.Time) +} + +// clockTimers represents a list of sortable timers. +type clockTimers []clockTimer + +func (a clockTimers) Len() int { return len(a) } +func (a clockTimers) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a clockTimers) Less(i, j int) bool { return a[i].Next().Before(a[j].Next()) } + +// Timer represents a single event. +// The current time will be sent on C, unless the timer was created by AfterFunc. +type Timer struct { + C <-chan time.Time + c chan time.Time + timer *time.Timer // realtime impl, if set + next time.Time // next tick time + mock *Mock // mock clock, if set + fn func() // AfterFunc function, if set + stopped bool // True if stopped, false if running +} + +// Stop turns off the ticker. +func (t *Timer) Stop() bool { + if t.timer != nil { + return t.timer.Stop() + } + + t.mock.mu.Lock() + registered := !t.stopped + t.mock.removeClockTimer((*internalTimer)(t)) + t.stopped = true + t.mock.mu.Unlock() + return registered +} + +// Reset changes the expiry time of the timer +func (t *Timer) Reset(d time.Duration) bool { + if t.timer != nil { + return t.timer.Reset(d) + } + + t.mock.mu.Lock() + t.next = t.mock.now.Add(d) + defer t.mock.mu.Unlock() + + registered := !t.stopped + if t.stopped { + t.mock.timers = append(t.mock.timers, (*internalTimer)(t)) + } + + t.stopped = false + return registered +} + +type internalTimer Timer + +func (t *internalTimer) Next() time.Time { return t.next } +func (t *internalTimer) Tick(now time.Time) { + // a gosched() after ticking, to allow any consequences of the + // tick to complete + defer gosched() + + t.mock.mu.Lock() + if t.fn != nil { + // defer function execution until the lock is released, and + defer t.fn() + } else { + t.c <- now + } + t.mock.removeClockTimer((*internalTimer)(t)) + t.stopped = true + t.mock.mu.Unlock() +} + +// Ticker holds a channel that receives "ticks" at regular intervals. +type Ticker struct { + C <-chan time.Time + c chan time.Time + ticker *time.Ticker // realtime impl, if set + next time.Time // next tick time + mock *Mock // mock clock, if set + d time.Duration // time between ticks +} + +// Stop turns off the ticker. +func (t *Ticker) Stop() { + if t.ticker != nil { + t.ticker.Stop() + } else { + t.mock.mu.Lock() + t.mock.removeClockTimer((*internalTicker)(t)) + t.mock.mu.Unlock() + } +} + +// Reset resets the ticker to a new duration. +func (t *Ticker) Reset(dur time.Duration) { + if t.ticker != nil { + t.ticker.Reset(dur) + return + } + + t.mock.mu.Lock() + defer t.mock.mu.Unlock() + + t.d = dur + t.next = t.mock.now.Add(dur) +} + +type internalTicker Ticker + +func (t *internalTicker) Next() time.Time { return t.next } +func (t *internalTicker) Tick(now time.Time) { + select { + case t.c <- now: + default: + } + t.next = now.Add(t.d) + gosched() +} + +// Sleep momentarily so that other goroutines can process. +func gosched() { time.Sleep(1 * time.Millisecond) } + +var ( + // type checking + _ Clock = &Mock{} +) diff --git a/vendor/github.com/benbjohnson/clock/context.go b/vendor/github.com/benbjohnson/clock/context.go new file mode 100644 index 000000000..eb67594f2 --- /dev/null +++ b/vendor/github.com/benbjohnson/clock/context.go @@ -0,0 +1,86 @@ +package clock + +import ( + "context" + "fmt" + "sync" + "time" +) + +func (m *Mock) WithTimeout(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) { + return m.WithDeadline(parent, m.Now().Add(timeout)) +} + +func (m *Mock) WithDeadline(parent context.Context, deadline time.Time) (context.Context, context.CancelFunc) { + if cur, ok := parent.Deadline(); ok && cur.Before(deadline) { + // The current deadline is already sooner than the new one. + return context.WithCancel(parent) + } + ctx := &timerCtx{clock: m, parent: parent, deadline: deadline, done: make(chan struct{})} + propagateCancel(parent, ctx) + dur := m.Until(deadline) + if dur <= 0 { + ctx.cancel(context.DeadlineExceeded) // deadline has already passed + return ctx, func() {} + } + ctx.Lock() + defer ctx.Unlock() + if ctx.err == nil { + ctx.timer = m.AfterFunc(dur, func() { + ctx.cancel(context.DeadlineExceeded) + }) + } + return ctx, func() { ctx.cancel(context.Canceled) } +} + +// propagateCancel arranges for child to be canceled when parent is. +func propagateCancel(parent context.Context, child *timerCtx) { + if parent.Done() == nil { + return // parent is never canceled + } + go func() { + select { + case <-parent.Done(): + child.cancel(parent.Err()) + case <-child.Done(): + } + }() +} + +type timerCtx struct { + sync.Mutex + + clock Clock + parent context.Context + deadline time.Time + done chan struct{} + + err error + timer *Timer +} + +func (c *timerCtx) cancel(err error) { + c.Lock() + defer c.Unlock() + if c.err != nil { + return // already canceled + } + c.err = err + close(c.done) + if c.timer != nil { + c.timer.Stop() + c.timer = nil + } +} + +func (c *timerCtx) Deadline() (deadline time.Time, ok bool) { return c.deadline, true } + +func (c *timerCtx) Done() <-chan struct{} { return c.done } + +func (c *timerCtx) Err() error { return c.err } + +func (c *timerCtx) Value(key interface{}) interface{} { return c.parent.Value(key) } + +func (c *timerCtx) String() string { + return fmt.Sprintf("clock.WithDeadline(%s [%s])", c.deadline, c.deadline.Sub(c.clock.Now())) +} diff --git a/vendor/go.uber.org/ratelimit/.gitignore b/vendor/go.uber.org/ratelimit/.gitignore index 01f986607..23bd48903 100644 --- a/vendor/go.uber.org/ratelimit/.gitignore +++ b/vendor/go.uber.org/ratelimit/.gitignore @@ -1,2 +1,11 @@ +/bin /vendor -/cover.out +cover.html +cover.out +profile.out +stat.csv +stat.txt +stat.html + +*.swp +.idea diff --git a/vendor/go.uber.org/ratelimit/CHANGELOG.md b/vendor/go.uber.org/ratelimit/CHANGELOG.md index 819e97ccf..6d265cbd2 100644 --- a/vendor/go.uber.org/ratelimit/CHANGELOG.md +++ b/vendor/go.uber.org/ratelimit/CHANGELOG.md @@ -4,6 +4,31 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## Unreleased +- No changes yet. + +## v0.3.1 - 2024-03-04 +### Fixed +- Fixed a bug related to maxSlack boundary detection. #124 + Thanks to @smallnest for reporting and @storozhukBM for fixing. + +## v0.3.0 - 2023-07-08 +### Changed +- Switched to a more efficient internal implementation. No API or behavior changes. + [#100](https://github.com/uber-go/ratelimit/pull/100) + +## v0.2.0 - 2021-03-02 +### Added +- Allow configuring the limiter with custom slack. + [#64](https://github.com/uber-go/ratelimit/pull/64) +- Allow configuring the limiter per arbitrary time duration. + [#54](https://github.com/uber-go/ratelimit/pull/54) +### Changed +- Switched from Glide to Go Modules. +### Fixed +- Fix not working slack. + [#60](https://github.com/uber-go/ratelimit/pull/60) + ## v0.1.0 ### Fixed - Changed the import path for `go.uber.org/atomic` to its newer, canonical diff --git a/vendor/go.uber.org/ratelimit/Makefile b/vendor/go.uber.org/ratelimit/Makefile index 4cae88189..7eb6d2468 100644 --- a/vendor/go.uber.org/ratelimit/Makefile +++ b/vendor/go.uber.org/ratelimit/Makefile @@ -1,5 +1,56 @@ -test: - go test . +# Directory to put `go install`ed binaries in. +export GOBIN ?= $(shell pwd)/bin + +GO_FILES := $(shell \ + find . '(' -path '*/.*' -o -path './vendor' ')' -prune \ + -o -name '*.go' -print | cut -b3-) + +.PHONY: bench +bench: bin/benchstat bin/benchart + go test -timeout 3h -count=5 -run=xxx -bench=BenchmarkRateLimiter ./... | tee stat.txt + @$(GOBIN)/benchstat stat.txt + @$(GOBIN)/benchstat -csv stat.txt > stat.csv + @$(GOBIN)/benchart 'RateLimiter;xAxisType=log' stat.csv stat.html + @open stat.html + +bin/benchstat: tools/go.mod + @cd tools && go install golang.org/x/perf/cmd/benchstat + +bin/benchart: tools/go.mod + @cd tools && go install github.com/storozhukBM/benchart + +bin/golint: tools/go.mod + @cd tools && go install golang.org/x/lint/golint + +bin/staticcheck: tools/go.mod + @cd tools && go install honnef.co/go/tools/cmd/staticcheck + +.PHONY: build +build: + go build ./... + +.PHONY: cover cover: - go test . -coverprofile=cover.out - go tool cover -html=cover.out + go test -coverprofile=cover.out -coverpkg=./... -v ./... + go tool cover -html=cover.out -o cover.html + +.PHONY: gofmt +gofmt: + $(eval FMT_LOG := $(shell mktemp -t gofmt.XXXXX)) + @gofmt -e -s -l $(GO_FILES) > $(FMT_LOG) || true + @[ ! -s "$(FMT_LOG)" ] || (echo "gofmt failed:" | cat - $(FMT_LOG) && false) + +.PHONY: golint +golint: bin/golint + @$(GOBIN)/golint -set_exit_status ./... + +.PHONY: lint +lint: gofmt golint staticcheck + +.PHONY: staticcheck +staticcheck: bin/staticcheck + @$(GOBIN)/staticcheck ./... + +.PHONY: test +test: + go test -race ./... diff --git a/vendor/go.uber.org/ratelimit/README.md b/vendor/go.uber.org/ratelimit/README.md index 290a6fa7a..84a273f9a 100644 --- a/vendor/go.uber.org/ratelimit/README.md +++ b/vendor/go.uber.org/ratelimit/README.md @@ -1,4 +1,4 @@ -# Go rate limiter +# Go rate limiter [![GoDoc][doc-img]][doc] [![Coverage Status][cov-img]][cov] ![test][test-img] This package provides a Golang implementation of the leaky-bucket rate limit algorithm. This implementation refills the bucket based on the time elapsed between @@ -38,3 +38,21 @@ func main() { // 9 10ms } ``` + +## FAQ: +- What's the major diff v.s. https://pkg.go.dev/golang.org/x/time/rate? (based on #77) + + This ratelimiter was meant to have a (1) simple API and (2) minimal overhead. For more complex use-cases [x/time/rate] is a great choice. See [here][redit] for historical context, and [here][bench] for benchmarks (from 2016). + +- Why does example_test.go fail when I run it locally on Windows? (based on #80) + + Windows has some known issues with timers precision. See golang/go#44343. We don't expect to work around it. + +[cov-img]: https://codecov.io/gh/uber-go/ratelimit/branch/master/graph/badge.svg?token=zhLeUjjrm2 +[cov]: https://codecov.io/gh/uber-go/ratelimit +[doc-img]: https://pkg.go.dev/badge/go.uber.org/ratelimit +[doc]: https://pkg.go.dev/go.uber.org/ratelimit +[test-img]: https://github.com/uber-go/ratelimit/workflows/test/badge.svg +[redit]: https://www.reddit.com/r/golang/comments/59k2bi/ubergoratelimit_a_golang_blocking_leakybucket/d99ob9q +[x/time/rate]: https://pkg.go.dev/golang.org/x/time/rate +[bench]: https://gist.github.com/prashantv/26016a7dbc6fc1ec52d8c2b6591f3582 diff --git a/vendor/go.uber.org/ratelimit/glide.lock b/vendor/go.uber.org/ratelimit/glide.lock deleted file mode 100644 index 62f9985cc..000000000 --- a/vendor/go.uber.org/ratelimit/glide.lock +++ /dev/null @@ -1,20 +0,0 @@ -hash: 4dabcf35c29ffecd1b84d2e6bee517c9749933c606e8ab6f21a46e694374dd7b -updated: 2019-07-29T11:36:27.328149-07:00 -imports: [] -testImports: -- name: github.com/davecgh/go-spew - version: d8f796af33cc11cb798c1aaeb27a4ebc5099927d - subpackages: - - spew -- name: github.com/pmezard/go-difflib - version: 5d4384ee4fb2527b0a1256a821ebfc92f91efefc - subpackages: - - difflib -- name: github.com/stretchr/testify - version: 221dbe5ed46703ee255b1da0dec05086f5035f62 - subpackages: - - assert -- name: go.uber.org/atomic - version: df976f2515e274675050de7b3f42545de80594fd -- name: gopkg.in/yaml.v2 - version: 51d6538a90f86fe93ac480b35f37b2be17fef232 diff --git a/vendor/go.uber.org/ratelimit/glide.yaml b/vendor/go.uber.org/ratelimit/glide.yaml deleted file mode 100644 index df0a2f6d0..000000000 --- a/vendor/go.uber.org/ratelimit/glide.yaml +++ /dev/null @@ -1,7 +0,0 @@ -package: go.uber.org/ratelimit -import: [] -testImport: -- package: github.com/stretchr/testify - subpackages: - - assert -- package: go.uber.org/atomic diff --git a/vendor/go.uber.org/ratelimit/internal/clock/clock.go b/vendor/go.uber.org/ratelimit/internal/clock/clock.go deleted file mode 100644 index 17569d886..000000000 --- a/vendor/go.uber.org/ratelimit/internal/clock/clock.go +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) 2016 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -package clock - -// Forked from github.com/andres-erbsen/clock to isolate a missing nap. - -import ( - "container/heap" - "sync" - "time" -) - -// Mock represents a mock clock that only moves forward programmically. -// It can be preferable to a real-time clock when testing time-based functionality. -type Mock struct { - sync.Mutex - now time.Time // current time - timers Timers // timers -} - -// NewMock returns an instance of a mock clock. -// The current time of the mock clock on initialization is the Unix epoch. -func NewMock() *Mock { - return &Mock{now: time.Unix(0, 0)} -} - -// Add moves the current time of the mock clock forward by the duration. -// This should only be called from a single goroutine at a time. -func (m *Mock) Add(d time.Duration) { - m.Lock() - // Calculate the final time. - end := m.now.Add(d) - - for len(m.timers) > 0 && m.now.Before(end) { - t := heap.Pop(&m.timers).(*Timer) - m.now = t.next - m.Unlock() - t.Tick() - m.Lock() - } - - m.Unlock() - // Give a small buffer to make sure the other goroutines get handled. - nap() -} - -// Timer produces a timer that will emit a time some duration after now. -func (m *Mock) Timer(d time.Duration) *Timer { - ch := make(chan time.Time) - t := &Timer{ - C: ch, - c: ch, - mock: m, - next: m.now.Add(d), - } - m.addTimer(t) - return t -} - -func (m *Mock) addTimer(t *Timer) { - m.Lock() - defer m.Unlock() - heap.Push(&m.timers, t) -} - -// After produces a channel that will emit the time after a duration passes. -func (m *Mock) After(d time.Duration) <-chan time.Time { - return m.Timer(d).C -} - -// AfterFunc waits for the duration to elapse and then executes a function. -// A Timer is returned that can be stopped. -func (m *Mock) AfterFunc(d time.Duration, f func()) *Timer { - t := m.Timer(d) - go func() { - <-t.c - f() - }() - nap() - return t -} - -// Now returns the current wall time on the mock clock. -func (m *Mock) Now() time.Time { - m.Lock() - defer m.Unlock() - return m.now -} - -// Sleep pauses the goroutine for the given duration on the mock clock. -// The clock must be moved forward in a separate goroutine. -func (m *Mock) Sleep(d time.Duration) { - <-m.After(d) -} - -// Timer represents a single event. -type Timer struct { - C <-chan time.Time - c chan time.Time - next time.Time // next tick time - mock *Mock // mock clock -} - -func (t *Timer) Next() time.Time { return t.next } - -func (t *Timer) Tick() { - select { - case t.c <- t.next: - default: - } - nap() -} - -// Sleep momentarily so that other goroutines can process. -func nap() { time.Sleep(1 * time.Millisecond) } diff --git a/vendor/go.uber.org/ratelimit/internal/clock/interface.go b/vendor/go.uber.org/ratelimit/internal/clock/interface.go deleted file mode 100644 index d380b3bb4..000000000 --- a/vendor/go.uber.org/ratelimit/internal/clock/interface.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2016 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -package clock - -import "time" - -// Clock represents an interface to the functions in the standard library time -// package. Two implementations are available in the clock package. The first -// is a real-time clock which simply wraps the time package's functions. The -// second is a mock clock which will only make forward progress when -// programmatically adjusted. -type Clock interface { - AfterFunc(d time.Duration, f func()) - Now() time.Time - Sleep(d time.Duration) -} diff --git a/vendor/go.uber.org/ratelimit/internal/clock/real.go b/vendor/go.uber.org/ratelimit/internal/clock/real.go deleted file mode 100644 index 3a1be7e26..000000000 --- a/vendor/go.uber.org/ratelimit/internal/clock/real.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2016 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -package clock - -import "time" - -// clock implements a real-time clock by simply wrapping the time package functions. -type clock struct{} - -// New returns an instance of a real-time clock. -func New() Clock { - return &clock{} -} - -func (c *clock) After(d time.Duration) <-chan time.Time { return time.After(d) } - -func (c *clock) AfterFunc(d time.Duration, f func()) { - // TODO maybe return timer interface - time.AfterFunc(d, f) -} - -func (c *clock) Now() time.Time { return time.Now() } - -func (c *clock) Sleep(d time.Duration) { time.Sleep(d) } diff --git a/vendor/go.uber.org/ratelimit/internal/clock/timers.go b/vendor/go.uber.org/ratelimit/internal/clock/timers.go deleted file mode 100644 index 577dd395d..000000000 --- a/vendor/go.uber.org/ratelimit/internal/clock/timers.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2016 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -package clock - -// timers represents a list of sortable timers. -type Timers []*Timer - -func (ts Timers) Len() int { return len(ts) } - -func (ts Timers) Swap(i, j int) { - ts[i], ts[j] = ts[j], ts[i] -} - -func (ts Timers) Less(i, j int) bool { - return ts[i].Next().Before(ts[j].Next()) -} - -func (ts *Timers) Push(t interface{}) { - *ts = append(*ts, t.(*Timer)) -} - -func (ts *Timers) Pop() interface{} { - t := (*ts)[len(*ts)-1] - *ts = (*ts)[:len(*ts)-1] - return t -} diff --git a/vendor/go.uber.org/ratelimit/limiter_atomic.go b/vendor/go.uber.org/ratelimit/limiter_atomic.go new file mode 100644 index 000000000..ac6465cad --- /dev/null +++ b/vendor/go.uber.org/ratelimit/limiter_atomic.go @@ -0,0 +1,110 @@ +// Copyright (c) 2016,2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package ratelimit // import "go.uber.org/ratelimit" + +import ( + "time" + + "sync/atomic" + "unsafe" +) + +type state struct { + last time.Time + sleepFor time.Duration +} + +type atomicLimiter struct { + state unsafe.Pointer + //lint:ignore U1000 Padding is unused but it is crucial to maintain performance + // of this rate limiter in case of collocation with other frequently accessed memory. + padding [56]byte // cache line size - state pointer size = 64 - 8; created to avoid false sharing. + + perRequest time.Duration + maxSlack time.Duration + clock Clock +} + +// newAtomicBased returns a new atomic based limiter. +func newAtomicBased(rate int, opts ...Option) *atomicLimiter { + // TODO consider moving config building to the implementation + // independent code. + config := buildConfig(opts) + perRequest := config.per / time.Duration(rate) + l := &atomicLimiter{ + perRequest: perRequest, + maxSlack: -1 * time.Duration(config.slack) * perRequest, + clock: config.clock, + } + + initialState := state{ + last: time.Time{}, + sleepFor: 0, + } + atomic.StorePointer(&l.state, unsafe.Pointer(&initialState)) + return l +} + +// Take blocks to ensure that the time spent between multiple +// Take calls is on average per/rate. +func (t *atomicLimiter) Take() time.Time { + var ( + newState state + taken bool + interval time.Duration + ) + for !taken { + now := t.clock.Now() + + previousStatePointer := atomic.LoadPointer(&t.state) + oldState := (*state)(previousStatePointer) + + newState = state{ + last: now, + sleepFor: oldState.sleepFor, + } + + // If this is our first request, then we allow it. + if oldState.last.IsZero() { + taken = atomic.CompareAndSwapPointer(&t.state, previousStatePointer, unsafe.Pointer(&newState)) + continue + } + + // sleepFor calculates how much time we should sleep based on + // the perRequest budget and how long the last request took. + // Since the request may take longer than the budget, this number + // can get negative, and is summed across requests. + newState.sleepFor += t.perRequest - now.Sub(oldState.last) + // We shouldn't allow sleepFor to get too negative, since it would mean that + // a service that slowed down a lot for a short period of time would get + // a much higher RPS following that. + if newState.sleepFor < t.maxSlack { + newState.sleepFor = t.maxSlack + } + if newState.sleepFor > 0 { + newState.last = newState.last.Add(newState.sleepFor) + interval, newState.sleepFor = newState.sleepFor, 0 + } + taken = atomic.CompareAndSwapPointer(&t.state, previousStatePointer, unsafe.Pointer(&newState)) + } + t.clock.Sleep(interval) + return newState.last +} diff --git a/vendor/go.uber.org/ratelimit/limiter_atomic_int64.go b/vendor/go.uber.org/ratelimit/limiter_atomic_int64.go new file mode 100644 index 000000000..8d370b732 --- /dev/null +++ b/vendor/go.uber.org/ratelimit/limiter_atomic_int64.go @@ -0,0 +1,92 @@ +// Copyright (c) 2022 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package ratelimit // import "go.uber.org/ratelimit" + +import ( + "sync/atomic" + "time" +) + +type atomicInt64Limiter struct { + //lint:ignore U1000 Padding is unused but it is crucial to maintain performance + // of this rate limiter in case of collocation with other frequently accessed memory. + prepadding [64]byte // cache line size = 64; created to avoid false sharing. + state int64 // unix nanoseconds of the next permissions issue. + //lint:ignore U1000 like prepadding. + postpadding [56]byte // cache line size - state size = 64 - 8; created to avoid false sharing. + + perRequest time.Duration + maxSlack time.Duration + clock Clock +} + +// newAtomicBased returns a new atomic based limiter. +func newAtomicInt64Based(rate int, opts ...Option) *atomicInt64Limiter { + // TODO consider moving config building to the implementation + // independent code. + config := buildConfig(opts) + perRequest := config.per / time.Duration(rate) + l := &atomicInt64Limiter{ + perRequest: perRequest, + maxSlack: time.Duration(config.slack) * perRequest, + clock: config.clock, + } + atomic.StoreInt64(&l.state, 0) + return l +} + +// Take blocks to ensure that the time spent between multiple +// Take calls is on average time.Second/rate. +func (t *atomicInt64Limiter) Take() time.Time { + var ( + newTimeOfNextPermissionIssue int64 + now int64 + ) + for { + now = t.clock.Now().UnixNano() + timeOfNextPermissionIssue := atomic.LoadInt64(&t.state) + + switch { + case timeOfNextPermissionIssue == 0 || (t.maxSlack == 0 && now-timeOfNextPermissionIssue > int64(t.perRequest)): + // if this is our first call or t.maxSlack == 0 we need to shrink issue time to now + newTimeOfNextPermissionIssue = now + case t.maxSlack > 0 && now-timeOfNextPermissionIssue > int64(t.maxSlack)+int64(t.perRequest): + // a lot of nanoseconds passed since the last Take call + // we will limit max accumulated time to maxSlack + newTimeOfNextPermissionIssue = now - int64(t.maxSlack) + default: + // calculate the time at which our permission was issued + newTimeOfNextPermissionIssue = timeOfNextPermissionIssue + int64(t.perRequest) + } + + if atomic.CompareAndSwapInt64(&t.state, timeOfNextPermissionIssue, newTimeOfNextPermissionIssue) { + break + } + } + + sleepDuration := time.Duration(newTimeOfNextPermissionIssue - now) + if sleepDuration > 0 { + t.clock.Sleep(sleepDuration) + return time.Unix(0, newTimeOfNextPermissionIssue) + } + // return now if we don't sleep as atomicLimiter does + return time.Unix(0, now) +} diff --git a/vendor/go.uber.org/ratelimit/limiter_mutexbased.go b/vendor/go.uber.org/ratelimit/limiter_mutexbased.go new file mode 100644 index 000000000..7bd18f793 --- /dev/null +++ b/vendor/go.uber.org/ratelimit/limiter_mutexbased.go @@ -0,0 +1,88 @@ +// Copyright (c) 2016,2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package ratelimit // import "go.uber.org/ratelimit" + +import ( + "sync" + "time" +) + +type mutexLimiter struct { + sync.Mutex + last time.Time + sleepFor time.Duration + perRequest time.Duration + maxSlack time.Duration + clock Clock +} + +// newMutexBased returns a new mutex based limiter. +func newMutexBased(rate int, opts ...Option) *mutexLimiter { + // TODO consider moving config building to the implementation + // independent code. + config := buildConfig(opts) + perRequest := config.per / time.Duration(rate) + l := &mutexLimiter{ + perRequest: perRequest, + maxSlack: -1 * time.Duration(config.slack) * perRequest, + clock: config.clock, + } + return l +} + +// Take blocks to ensure that the time spent between multiple +// Take calls is on average per/rate. +func (t *mutexLimiter) Take() time.Time { + t.Lock() + defer t.Unlock() + + now := t.clock.Now() + + // If this is our first request, then we allow it. + if t.last.IsZero() { + t.last = now + return t.last + } + + // sleepFor calculates how much time we should sleep based on + // the perRequest budget and how long the last request took. + // Since the request may take longer than the budget, this number + // can get negative, and is summed across requests. + t.sleepFor += t.perRequest - now.Sub(t.last) + + // We shouldn't allow sleepFor to get too negative, since it would mean that + // a service that slowed down a lot for a short period of time would get + // a much higher RPS following that. + if t.sleepFor < t.maxSlack { + t.sleepFor = t.maxSlack + } + + // If sleepFor is positive, then we should sleep now. + if t.sleepFor > 0 { + t.clock.Sleep(t.sleepFor) + t.last = now.Add(t.sleepFor) + t.sleepFor = 0 + } else { + t.last = now + } + + return t.last +} diff --git a/vendor/go.uber.org/ratelimit/ratelimit.go b/vendor/go.uber.org/ratelimit/ratelimit.go index 27698566d..22b88ecd5 100644 --- a/vendor/go.uber.org/ratelimit/ratelimit.go +++ b/vendor/go.uber.org/ratelimit/ratelimit.go @@ -1,4 +1,4 @@ -// Copyright (c) 2016 Uber Technologies, Inc. +// Copyright (c) 2016,2020 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -21,10 +21,9 @@ package ratelimit // import "go.uber.org/ratelimit" import ( - "sync" "time" - "go.uber.org/ratelimit/internal/clock" + "github.com/benbjohnson/clock" ) // Note: This file is inspired by: @@ -46,86 +45,82 @@ type Clock interface { Sleep(time.Duration) } -type limiter struct { - sync.Mutex - last time.Time - sleepFor time.Duration - perRequest time.Duration - maxSlack time.Duration - clock Clock +// config configures a limiter. +type config struct { + clock Clock + slack int + per time.Duration } -// Option configures a Limiter. -type Option func(l *limiter) - // New returns a Limiter that will limit to the given RPS. func New(rate int, opts ...Option) Limiter { - l := &limiter{ - perRequest: time.Second / time.Duration(rate), - maxSlack: -10 * time.Second / time.Duration(rate), + return newAtomicInt64Based(rate, opts...) +} + +// buildConfig combines defaults with options. +func buildConfig(opts []Option) config { + c := config{ + clock: clock.New(), + slack: 10, + per: time.Second, } + for _, opt := range opts { - opt(l) - } - if l.clock == nil { - l.clock = clock.New() + opt.apply(&c) } - return l + return c +} + +// Option configures a Limiter. +type Option interface { + apply(*config) +} + +type clockOption struct { + clock Clock +} + +func (o clockOption) apply(c *config) { + c.clock = o.clock } // WithClock returns an option for ratelimit.New that provides an alternate // Clock implementation, typically a mock Clock for testing. func WithClock(clock Clock) Option { - return func(l *limiter) { - l.clock = clock - } + return clockOption{clock: clock} } -// WithoutSlack is an option for ratelimit.New that initializes the limiter -// without any initial tolerance for bursts of traffic. -var WithoutSlack Option = withoutSlackOption +type slackOption int -func withoutSlackOption(l *limiter) { - l.maxSlack = 0 +func (o slackOption) apply(c *config) { + c.slack = int(o) } -// Take blocks to ensure that the time spent between multiple -// Take calls is on average time.Second/rate. -func (t *limiter) Take() time.Time { - t.Lock() - defer t.Unlock() +// WithoutSlack configures the limiter to be strict and not to accumulate +// previously "unspent" requests for future bursts of traffic. +var WithoutSlack Option = slackOption(0) - now := t.clock.Now() +// WithSlack configures custom slack. +// Slack allows the limiter to accumulate "unspent" requests +// for future bursts of traffic. +func WithSlack(slack int) Option { + return slackOption(slack) +} - // If this is our first request, then we allow it. - if t.last.IsZero() { - t.last = now - return t.last - } +type perOption time.Duration - // sleepFor calculates how much time we should sleep based on - // the perRequest budget and how long the last request took. - // Since the request may take longer than the budget, this number - // can get negative, and is summed across requests. - t.sleepFor += t.perRequest - now.Sub(t.last) - - // We shouldn't allow sleepFor to get too negative, since it would mean that - // a service that slowed down a lot for a short period of time would get - // a much higher RPS following that. - if t.sleepFor < t.maxSlack { - t.sleepFor = t.maxSlack - } - - // If sleepFor is positive, then we should sleep now. - if t.sleepFor > 0 { - t.clock.Sleep(t.sleepFor) - t.last = now.Add(t.sleepFor) - t.sleepFor = 0 - } else { - t.last = now - } +func (p perOption) apply(c *config) { + c.per = time.Duration(p) +} - return t.last +// Per allows configuring limits for different time windows. +// +// The default window is one second, so New(100) produces a one hundred per +// second (100 Hz) rate limiter. +// +// New(2, Per(60*time.Second)) creates a 2 per minute rate limiter. +func Per(per time.Duration) Option { + return perOption(per) } type unlimited struct{} diff --git a/vendor/modules.txt b/vendor/modules.txt index 13a16303e..0450cfcd6 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -64,6 +64,9 @@ github.com/aliyun/credentials-go/credentials/providers github.com/aliyun/credentials-go/credentials/request github.com/aliyun/credentials-go/credentials/response github.com/aliyun/credentials-go/credentials/utils +# github.com/benbjohnson/clock v1.3.0 +## explicit; go 1.15 +github.com/benbjohnson/clock # github.com/beorn7/perks v1.0.1 ## explicit; go 1.11 github.com/beorn7/perks/quantile @@ -264,10 +267,9 @@ go.opentelemetry.io/otel/trace/embedded # go.uber.org/multierr v1.11.0 ## explicit; go 1.19 go.uber.org/multierr -# go.uber.org/ratelimit v0.1.0 -## explicit +# go.uber.org/ratelimit v0.3.1 +## explicit; go 1.20 go.uber.org/ratelimit -go.uber.org/ratelimit/internal/clock # go.uber.org/zap v1.27.0 ## explicit; go 1.19 go.uber.org/zap