From 294918f487d6dbee1fa392a85fd280b8805f5b57 Mon Sep 17 00:00:00 2001 From: Denis Krivosheev Date: Mon, 27 Feb 2023 18:23:05 +0300 Subject: [PATCH 01/13] Introduce rate limiter - Add configuration parameters for rate limiter. - LRU cache is used for storing rate limiting information. - Apply rate limiter in Transport. --- cmd/static/default-config.toml | 11 ++++++ go.mod | 2 + go.sum | 4 ++ internal/proxy/config.go | 18 ++++++++- internal/proxy/rate_limiter.go | 68 ++++++++++++++++++++++++++++++++++ internal/proxy/transport.go | 13 +++++++ vendor/modules.txt | 9 +++++ 7 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 internal/proxy/rate_limiter.go diff --git a/cmd/static/default-config.toml b/cmd/static/default-config.toml index fd647179..46b463b0 100644 --- a/cmd/static/default-config.toml +++ b/cmd/static/default-config.toml @@ -107,6 +107,17 @@ HTTPSBackend = false # Ignore backend https certificate validations if HTTPSBackend is true HTTPSBackendIgnoreCert = true +# Maximum amount of requests per host in a unit of time defined at "RateLimitTimeWindow". +# 0 means no rate limit +RateLimit = 0 +# Value in milliseconds +RateLimitTimeWindow = 1000 +# The number of requests per host that can be handled in paralles. The default value 0 +# means the value will be the same as RateLimit +RateLimitBurst = 0 +# The size of LRU cache for the rate limiting information +RateLimitCacheSize = 100000 + [CheckDomains] # Allow domain if it resolver for one of public IPs of this server. diff --git a/go.mod b/go.mod index 2adee48d..99dbf255 100644 --- a/go.mod +++ b/go.mod @@ -23,8 +23,10 @@ require ( ) require ( + github.com/hashicorp/golang-lru/v2 v2.0.1 github.com/letsencrypt/pebble/v2 v2.4.0 github.com/rekby/fastuuid v0.9.0 + golang.org/x/time v0.3.0 ) require ( diff --git a/go.sum b/go.sum index 5ab2cce6..d4e1a489 100644 --- a/go.sum +++ b/go.sum @@ -135,6 +135,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnxuVTqZ6x4= +github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hexdigest/gowrap v1.1.7/go.mod h1:Z+nBFUDLa01iaNM+/jzoOA1JJ7sm51rnYFauKFUB5fs= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -417,6 +419,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/internal/proxy/config.go b/internal/proxy/config.go index c76bcd21..df9ebcba 100644 --- a/internal/proxy/config.go +++ b/internal/proxy/config.go @@ -26,6 +26,10 @@ type Config struct { HTTPSBackend bool HTTPSBackendIgnoreCert bool EnableAccessLog bool + RateLimit int + RateLimitTimeWindow int + RateLimitBurst int + RateLimitCacheSize int } func (c *Config) Apply(ctx context.Context, p *HTTPProxy) error { @@ -42,11 +46,21 @@ func (c *Config) Apply(ctx context.Context, p *HTTPProxy) error { chain = append(chain, director) } + rateLimiter, resErr := NewRateLimiter( + c.RateLimit, + c.RateLimitTimeWindow, + c.RateLimitBurst, + c.RateLimitCacheSize, + ) + appendDirector(c.getDefaultTargetDirector) appendDirector(c.getMapDirector) appendDirector(c.getHeadersDirector) appendDirector(c.getSchemaDirector) - p.HTTPTransport = Transport{c.HTTPSBackendIgnoreCert} + p.HTTPTransport = Transport{ + IgnoreHTTPSCertificate: c.HTTPSBackendIgnoreCert, + RateLimiter: rateLimiter, + } p.EnableAccessLog = c.EnableAccessLog if resErr != nil { @@ -90,7 +104,7 @@ func (c *Config) getDefaultTargetDirector(ctx context.Context) (Director, error) return NewDirectorHost(defaultTarget.String()), nil } -//can return nil,nil +// can return nil,nil func (c *Config) getHeadersDirector(ctx context.Context) (Director, error) { logger := zc.L(ctx) diff --git a/internal/proxy/rate_limiter.go b/internal/proxy/rate_limiter.go new file mode 100644 index 00000000..3c6f7eaf --- /dev/null +++ b/internal/proxy/rate_limiter.go @@ -0,0 +1,68 @@ +package proxy + +import ( + "net/http" + "sync" + + "github.com/hashicorp/golang-lru/v2" + "golang.org/x/time/rate" +) + +type RateLimiter struct { + rateLimit int + timeWindow int + burst int + + mx sync.RWMutex + cache *lru.Cache[string, *rate.Limiter] +} + +func NewRateLimiter(rateLimit, timeWindow, burst, cacheSize int) (*RateLimiter, error) { + if rateLimit == 0 { + return &RateLimiter{}, nil + } + + cache, err := lru.New[string, *rate.Limiter](cacheSize) + if err != nil { + return nil, err + } + + return &RateLimiter{ + rateLimit: rateLimit, + timeWindow: timeWindow, + burst: burst, + cache: cache, + }, nil +} + +func (rl *RateLimiter) Wait(r *http.Request) error { + if rl.rateLimit == 0 { + return nil + } + + return rl.getLimiter(r).Wait(r.Context()) +} + +func (rl *RateLimiter) getLimiter(r *http.Request) *rate.Limiter { + rl.mx.RLock() + ip := getIP(r) + + limiter, ok := rl.cache.Get(ip) + if ok { + rl.mx.RUnlock() + return limiter + } + + rl.mx.RUnlock() + rl.mx.Lock() + defer rl.mx.Unlock() + + limiter = rate.NewLimiter(rate.Limit(rl.rateLimit*1000/rl.timeWindow), rl.burst) + rl.cache.Add(ip, limiter) + + return limiter +} + +func getIP(r *http.Request) string { + return r.RemoteAddr +} diff --git a/internal/proxy/transport.go b/internal/proxy/transport.go index 33e62dbf..b0d2decf 100644 --- a/internal/proxy/transport.go +++ b/internal/proxy/transport.go @@ -16,9 +16,22 @@ var defaultHTTPTransport = defaultTransport() type Transport struct { IgnoreHTTPSCertificate bool + RateLimiter *RateLimiter } func (t Transport) RoundTrip(req *http.Request) (*http.Response, error) { + if err := t.RateLimiter.Wait(req); err != nil { + return &http.Response{ + Status: "429 Too Many Requests", + StatusCode: http.StatusTooManyRequests, + Proto: req.Proto, + ProtoMajor: req.ProtoMajor, + ProtoMinor: req.ProtoMinor, + Request: req, + Header: make(http.Header, 0), + }, nil + } + return t.getTransport(req).RoundTrip(req) } diff --git a/vendor/modules.txt b/vendor/modules.txt index fb3d35c5..9f8c3a87 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -63,6 +63,10 @@ github.com/gojuno/minimock/v3 ## explicit; go 1.9 github.com/golang/protobuf/proto github.com/golang/protobuf/ptypes/timestamp +# github.com/hashicorp/golang-lru/v2 v2.0.1 +## explicit; go 1.18 +github.com/hashicorp/golang-lru/v2 +github.com/hashicorp/golang-lru/v2/simplelru # github.com/jmespath/go-jmespath v0.4.0 ## explicit; go 1.14 github.com/jmespath/go-jmespath @@ -183,6 +187,11 @@ golang.org/x/text/secure/bidirule golang.org/x/text/transform golang.org/x/text/unicode/bidi golang.org/x/text/unicode/norm +# golang.org/x/time v0.3.0 +## explicit +golang.org/x/time/rate +# golang.org/x/tools v0.1.11 +## explicit; go 1.17 # golang.org/x/tools v0.6.0 ## explicit; go 1.18 golang.org/x/tools/go/gcexportdata From ae51a22b0d2927b58e2ed84d4f0d1408c7c6c7c2 Mon Sep 17 00:00:00 2001 From: Denis Krivosheev Date: Thu, 23 Mar 2023 12:39:02 +0300 Subject: [PATCH 02/13] Fix data race when accessing the rate limiter cache --- internal/proxy/rate_limiter.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/proxy/rate_limiter.go b/internal/proxy/rate_limiter.go index 3c6f7eaf..651fd1f1 100644 --- a/internal/proxy/rate_limiter.go +++ b/internal/proxy/rate_limiter.go @@ -57,8 +57,12 @@ func (rl *RateLimiter) getLimiter(r *http.Request) *rate.Limiter { rl.mx.Lock() defer rl.mx.Unlock() - limiter = rate.NewLimiter(rate.Limit(rl.rateLimit*1000/rl.timeWindow), rl.burst) - rl.cache.Add(ip, limiter) + // we need to check cache again to avoid data race + limiter, ok = rl.cache.Get(ip) + if !ok { + limiter = rate.NewLimiter(rate.Limit(rl.rateLimit*1000/rl.timeWindow), rl.burst) + rl.cache.Add(ip, limiter) + } return limiter } From d418464fd1c377012e683b83575eed496f8d7f19 Mon Sep 17 00:00:00 2001 From: Denis Krivosheev Date: Wed, 29 Mar 2023 10:04:27 +0300 Subject: [PATCH 03/13] Tests for rate limiter --- internal/proxy/rate_limiter_test.go | 114 ++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 internal/proxy/rate_limiter_test.go diff --git a/internal/proxy/rate_limiter_test.go b/internal/proxy/rate_limiter_test.go new file mode 100644 index 00000000..3ed0eff2 --- /dev/null +++ b/internal/proxy/rate_limiter_test.go @@ -0,0 +1,114 @@ +package proxy + +import ( + "context" + "net/http" + "sync" + "sync/atomic" + "testing" + + "github.com/maxatome/go-testdeep" +) + +func TestRateLimiter_Wait(t *testing.T) { + req1, _ := http.NewRequest("GET", "http://url1.com", nil) + req1.RemoteAddr = "ip1" + + req2, _ := http.NewRequest("GET", "http://url2.com", nil) + req1.RemoteAddr = "ip2" + + ctx, close := context.WithCancel(context.Background()) + close() + reqWithClosedCtx, _ := http.NewRequestWithContext(ctx, "GET", "http://with-closed-ctx.com", nil) + reqWithClosedCtx.RemoteAddr = "with-closed-ctx" + + type want struct { + errorNumber int32 + cacheLen int + } + tests := []struct { + name string + + rateLimit int + timeWindow int + burst int + cacheSize int + + requests []*http.Request + + want want + }{ + { + name: "queries with the same IP should be handled sequentially", + + rateLimit: 1, + timeWindow: 1, + burst: 1, + cacheSize: 100, + + requests: []*http.Request{req1, req1, req1}, + + want: want{ + errorNumber: 0, + cacheLen: 1, + }, + }, + { + name: "queries with different IPs should be handled in parallel", + + rateLimit: 1, + timeWindow: 1, + burst: 1, + cacheSize: 100, + + requests: []*http.Request{req1, req1, req2}, + + want: want{ + errorNumber: 0, + cacheLen: 2, + }, + }, + { + name: "waiting should fail in case the context is done", + + rateLimit: 1, + timeWindow: 1, + burst: 1, + cacheSize: 100, + + requests: []*http.Request{req1, reqWithClosedCtx, req2}, + + want: want{ + errorNumber: 1, + cacheLen: 3, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + limiter, err := NewRateLimiter(tt.rateLimit, tt.timeWindow, tt.burst, tt.cacheSize) + testdeep.CmpNoError(t, err) + + var errCounter int32 = 0 + var wg sync.WaitGroup + + for _, req := range tt.requests { + req := req + wg.Add(1) + go func() { + err := limiter.Wait(req) + if err != nil { + atomic.AddInt32(&errCounter, 1) + } + wg.Done() + }() + } + + wg.Wait() + + testdeep.Cmp(t, limiter.cache.Len(), tt.want.cacheLen, "incorrect cache length") + testdeep.Cmp(t, errCounter, tt.want.errorNumber, "incorrect number of errors") + }) + } +} From a790f6c7cabf1c657a04f8bd11561f4c4c845f70 Mon Sep 17 00:00:00 2001 From: Denis Krivosheev Date: Fri, 31 Mar 2023 12:39:09 +0300 Subject: [PATCH 04/13] Use "Allow" semantic instead of the "Wait" one Immediately send the request if it is allowed and immediately fail if not. --- internal/proxy/rate_limiter.go | 9 +- internal/proxy/rate_limiter_test.go | 146 ++++++++++++++++------------ internal/proxy/transport.go | 2 +- 3 files changed, 91 insertions(+), 66 deletions(-) diff --git a/internal/proxy/rate_limiter.go b/internal/proxy/rate_limiter.go index 651fd1f1..b839a6b3 100644 --- a/internal/proxy/rate_limiter.go +++ b/internal/proxy/rate_limiter.go @@ -35,12 +35,15 @@ func NewRateLimiter(rateLimit, timeWindow, burst, cacheSize int) (*RateLimiter, }, nil } -func (rl *RateLimiter) Wait(r *http.Request) error { +func (rl *RateLimiter) Allow(r *http.Request) bool { if rl.rateLimit == 0 { - return nil + return true + } + if r.Context().Err() != nil { + return false } - return rl.getLimiter(r).Wait(r.Context()) + return rl.getLimiter(r).Allow() } func (rl *RateLimiter) getLimiter(r *http.Request) *rate.Limiter { diff --git a/internal/proxy/rate_limiter_test.go b/internal/proxy/rate_limiter_test.go index 3ed0eff2..c99ef439 100644 --- a/internal/proxy/rate_limiter_test.go +++ b/internal/proxy/rate_limiter_test.go @@ -3,112 +3,134 @@ package proxy import ( "context" "net/http" - "sync" - "sync/atomic" "testing" + "time" "github.com/maxatome/go-testdeep" ) -func TestRateLimiter_Wait(t *testing.T) { +func TestMaxRequestsPerSec(t *testing.T) { req1, _ := http.NewRequest("GET", "http://url1.com", nil) req1.RemoteAddr = "ip1" req2, _ := http.NewRequest("GET", "http://url2.com", nil) - req1.RemoteAddr = "ip2" + req2.RemoteAddr = "ip2" - ctx, close := context.WithCancel(context.Background()) - close() - reqWithClosedCtx, _ := http.NewRequestWithContext(ctx, "GET", "http://with-closed-ctx.com", nil) - reqWithClosedCtx.RemoteAddr = "with-closed-ctx" + ctx, cancel := context.WithCancel(context.Background()) + cancel() + canceledReq, _ := http.NewRequestWithContext(ctx, "GET", "http://canceled.com", nil) + canceledReq.RemoteAddr = "canceled" - type want struct { - errorNumber int32 - cacheLen int + type reqSpec struct { + req *http.Request + wantAllowedRequestAround int // expected the limiter to allow +-1 } + tests := []struct { name string rateLimit int timeWindow int - burst int - cacheSize int - - requests []*http.Request + testTime time.Duration - want want + reqSpecs []reqSpec }{ { - name: "queries with the same IP should be handled sequentially", + name: "should limit the amount of requests per second", - rateLimit: 1, - timeWindow: 1, - burst: 1, - cacheSize: 100, + rateLimit: 10, + timeWindow: 1000, + testTime: time.Second, - requests: []*http.Request{req1, req1, req1}, - - want: want{ - errorNumber: 0, - cacheLen: 1, + reqSpecs: []reqSpec{ + { + req: req1, + wantAllowedRequestAround: 10, + }, }, }, { - name: "queries with different IPs should be handled in parallel", - - rateLimit: 1, - timeWindow: 1, - burst: 1, - cacheSize: 100, + name: "should restart the timer for the next time window", - requests: []*http.Request{req1, req1, req2}, + rateLimit: 10, + timeWindow: 500, + testTime: time.Second, - want: want{ - errorNumber: 0, - cacheLen: 2, + reqSpecs: []reqSpec{ + { + req: req1, + wantAllowedRequestAround: 20, + }, }, }, { - name: "waiting should fail in case the context is done", - - rateLimit: 1, - timeWindow: 1, - burst: 1, - cacheSize: 100, + name: "requests from different IPs should NOT influence each other", + + rateLimit: 10, + timeWindow: 1000, + testTime: time.Second, + + reqSpecs: []reqSpec{ + { + req: req1, + wantAllowedRequestAround: 10, + }, + { + req: req2, + wantAllowedRequestAround: 10, + }, + }, + }, + { + name: "canceled request should always fail", - requests: []*http.Request{req1, reqWithClosedCtx, req2}, + rateLimit: 10, + timeWindow: 1000, + testTime: time.Second, - want: want{ - errorNumber: 1, - cacheLen: 3, + reqSpecs: []reqSpec{ + { + req: canceledReq, + wantAllowedRequestAround: 0, + }, }, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - limiter, err := NewRateLimiter(tt.rateLimit, tt.timeWindow, tt.burst, tt.cacheSize) + + // Preparations + limiter, err := NewRateLimiter(tt.rateLimit, tt.timeWindow, 1, 100) testdeep.CmpNoError(t, err) - var errCounter int32 = 0 - var wg sync.WaitGroup + endTime := time.Now().Add(tt.testTime) + successCounters := make([]int, len(tt.reqSpecs)) + reqCounters := make([]int, len(tt.reqSpecs)) - for _, req := range tt.requests { - req := req - wg.Add(1) - go func() { - err := limiter.Wait(req) - if err != nil { - atomic.AddInt32(&errCounter, 1) + // The test itself + for time.Now().Before(endTime) { + for idx, spec := range tt.reqSpecs { + reqCounters[idx]++ + if limiter.Allow(spec.req) { + successCounters[idx]++ } - wg.Done() - }() + } } - wg.Wait() - - testdeep.Cmp(t, limiter.cache.Len(), tt.want.cacheLen, "incorrect cache length") - testdeep.Cmp(t, errCounter, tt.want.errorNumber, "incorrect number of errors") + // Check the expectations + for idx, spec := range tt.reqSpecs { + testdeep.CmpBetween( + t, + successCounters[idx], + spec.wantAllowedRequestAround-1, + spec.wantAllowedRequestAround+1, + testdeep.BoundsInIn, + ) + testdeep.CmpGt(t, reqCounters[idx], successCounters[idx]) + } }) } + } diff --git a/internal/proxy/transport.go b/internal/proxy/transport.go index b0d2decf..08e12c2c 100644 --- a/internal/proxy/transport.go +++ b/internal/proxy/transport.go @@ -20,7 +20,7 @@ type Transport struct { } func (t Transport) RoundTrip(req *http.Request) (*http.Response, error) { - if err := t.RateLimiter.Wait(req); err != nil { + if !t.RateLimiter.Allow(req) { return &http.Response{ Status: "429 Too Many Requests", StatusCode: http.StatusTooManyRequests, From 8dad5eb47e503c010999027075e3d4380611633a Mon Sep 17 00:00:00 2001 From: Denis Krivosheev Date: Mon, 3 Apr 2023 09:33:06 +0300 Subject: [PATCH 05/13] Add whitespace between options in config --- cmd/static/default-config.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/static/default-config.toml b/cmd/static/default-config.toml index 46b463b0..7ef793ed 100644 --- a/cmd/static/default-config.toml +++ b/cmd/static/default-config.toml @@ -110,11 +110,14 @@ HTTPSBackendIgnoreCert = true # Maximum amount of requests per host in a unit of time defined at "RateLimitTimeWindow". # 0 means no rate limit RateLimit = 0 + # Value in milliseconds RateLimitTimeWindow = 1000 + # The number of requests per host that can be handled in paralles. The default value 0 # means the value will be the same as RateLimit RateLimitBurst = 0 + # The size of LRU cache for the rate limiting information RateLimitCacheSize = 100000 From 4b152a33a22cbd9f830402a1f2c53ae970256988 Mon Sep 17 00:00:00 2001 From: Denis Krivosheev Date: Mon, 3 Apr 2023 09:45:41 +0300 Subject: [PATCH 06/13] Use duration type for rate limit time window --- internal/proxy/config.go | 12 ++++++------ internal/proxy/rate_limiter.go | 24 ++++++++++++++++-------- internal/proxy/rate_limiter_test.go | 9 +++++++-- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/internal/proxy/config.go b/internal/proxy/config.go index df9ebcba..d01f533a 100644 --- a/internal/proxy/config.go +++ b/internal/proxy/config.go @@ -46,12 +46,12 @@ func (c *Config) Apply(ctx context.Context, p *HTTPProxy) error { chain = append(chain, director) } - rateLimiter, resErr := NewRateLimiter( - c.RateLimit, - c.RateLimitTimeWindow, - c.RateLimitBurst, - c.RateLimitCacheSize, - ) + rateLimiter, resErr := NewRateLimiter(RateLimitParams{ + RateLimit: c.RateLimit, + TimeWindow: time.Duration(c.RateLimitTimeWindow) * time.Millisecond, + Burst: c.RateLimitBurst, + CacheSize: c.RateLimitCacheSize, + }) appendDirector(c.getDefaultTargetDirector) appendDirector(c.getMapDirector) diff --git a/internal/proxy/rate_limiter.go b/internal/proxy/rate_limiter.go index b839a6b3..434fba33 100644 --- a/internal/proxy/rate_limiter.go +++ b/internal/proxy/rate_limiter.go @@ -3,6 +3,7 @@ package proxy import ( "net/http" "sync" + "time" "github.com/hashicorp/golang-lru/v2" "golang.org/x/time/rate" @@ -10,27 +11,34 @@ import ( type RateLimiter struct { rateLimit int - timeWindow int + timeWindow time.Duration burst int mx sync.RWMutex cache *lru.Cache[string, *rate.Limiter] } -func NewRateLimiter(rateLimit, timeWindow, burst, cacheSize int) (*RateLimiter, error) { - if rateLimit == 0 { +type RateLimitParams struct { + RateLimit int + TimeWindow time.Duration + Burst int + CacheSize int +} + +func NewRateLimiter(params RateLimitParams) (*RateLimiter, error) { + if params.RateLimit == 0 { return &RateLimiter{}, nil } - cache, err := lru.New[string, *rate.Limiter](cacheSize) + cache, err := lru.New[string, *rate.Limiter](params.CacheSize) if err != nil { return nil, err } return &RateLimiter{ - rateLimit: rateLimit, - timeWindow: timeWindow, - burst: burst, + rateLimit: params.RateLimit, + timeWindow: params.TimeWindow, + burst: params.Burst, cache: cache, }, nil } @@ -63,7 +71,7 @@ func (rl *RateLimiter) getLimiter(r *http.Request) *rate.Limiter { // we need to check cache again to avoid data race limiter, ok = rl.cache.Get(ip) if !ok { - limiter = rate.NewLimiter(rate.Limit(rl.rateLimit*1000/rl.timeWindow), rl.burst) + limiter = rate.NewLimiter(rate.Limit(float64(rl.rateLimit)/rl.timeWindow.Seconds()), rl.burst) rl.cache.Add(ip, limiter) } diff --git a/internal/proxy/rate_limiter_test.go b/internal/proxy/rate_limiter_test.go index c99ef439..cd9fc408 100644 --- a/internal/proxy/rate_limiter_test.go +++ b/internal/proxy/rate_limiter_test.go @@ -30,7 +30,7 @@ func TestMaxRequestsPerSec(t *testing.T) { name string rateLimit int - timeWindow int + timeWindow time.Duration testTime time.Duration reqSpecs []reqSpec @@ -102,7 +102,12 @@ func TestMaxRequestsPerSec(t *testing.T) { t.Parallel() // Preparations - limiter, err := NewRateLimiter(tt.rateLimit, tt.timeWindow, 1, 100) + limiter, err := NewRateLimiter(RateLimitParams{ + RateLimit: tt.rateLimit, + TimeWindow: tt.timeWindow, + Burst: 1, + CacheSize: 100, + }) testdeep.CmpNoError(t, err) endTime := time.Now().Add(tt.testTime) From afcc565ef01a3b330afcbc7e10b828820b405d2a Mon Sep 17 00:00:00 2001 From: Denis Krivosheev Date: Mon, 3 Apr 2023 09:48:45 +0300 Subject: [PATCH 07/13] Improve time calculations in rate limiter tests --- internal/proxy/rate_limiter_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/proxy/rate_limiter_test.go b/internal/proxy/rate_limiter_test.go index cd9fc408..7be418da 100644 --- a/internal/proxy/rate_limiter_test.go +++ b/internal/proxy/rate_limiter_test.go @@ -110,12 +110,12 @@ func TestMaxRequestsPerSec(t *testing.T) { }) testdeep.CmpNoError(t, err) - endTime := time.Now().Add(tt.testTime) + startTime := time.Now() successCounters := make([]int, len(tt.reqSpecs)) reqCounters := make([]int, len(tt.reqSpecs)) // The test itself - for time.Now().Before(endTime) { + for time.Since(startTime) < tt.testTime { for idx, spec := range tt.reqSpecs { reqCounters[idx]++ if limiter.Allow(spec.req) { From 215ce3aef74a6421b04e68a80534ebb0780189ca Mon Sep 17 00:00:00 2001 From: Denis Krivosheev Date: Mon, 3 Apr 2023 09:49:44 +0300 Subject: [PATCH 08/13] go mod vendor --- vendor/modules.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/vendor/modules.txt b/vendor/modules.txt index 9f8c3a87..ad463b15 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -190,8 +190,6 @@ golang.org/x/text/unicode/norm # golang.org/x/time v0.3.0 ## explicit golang.org/x/time/rate -# golang.org/x/tools v0.1.11 -## explicit; go 1.17 # golang.org/x/tools v0.6.0 ## explicit; go 1.18 golang.org/x/tools/go/gcexportdata From 8ef9cf7d46e7d5325c81f15239e90d4a3b474f5a Mon Sep 17 00:00:00 2001 From: Denis Krivosheev Date: Mon, 3 Apr 2023 10:35:51 +0300 Subject: [PATCH 09/13] Use fake clock for tests --- go.mod | 1 + go.sum | 2 ++ internal/proxy/rate_limiter.go | 16 +++++++++++++--- internal/proxy/rate_limiter_test.go | 17 +++++++++++------ vendor/modules.txt | 3 +++ 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 99dbf255..11a8e9a0 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( require ( github.com/hashicorp/golang-lru/v2 v2.0.1 + github.com/jonboulle/clockwork v0.4.0 github.com/letsencrypt/pebble/v2 v2.4.0 github.com/rekby/fastuuid v0.9.0 golang.org/x/time v0.3.0 diff --git a/go.sum b/go.sum index d4e1a489..ad6ed203 100644 --- a/go.sum +++ b/go.sum @@ -143,6 +143,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= diff --git a/internal/proxy/rate_limiter.go b/internal/proxy/rate_limiter.go index 434fba33..b6f56518 100644 --- a/internal/proxy/rate_limiter.go +++ b/internal/proxy/rate_limiter.go @@ -6,6 +6,7 @@ import ( "time" "github.com/hashicorp/golang-lru/v2" + "github.com/jonboulle/clockwork" "golang.org/x/time/rate" ) @@ -14,6 +15,7 @@ type RateLimiter struct { timeWindow time.Duration burst int + clock clockwork.Clock mx sync.RWMutex cache *lru.Cache[string, *rate.Limiter] } @@ -23,6 +25,7 @@ type RateLimitParams struct { TimeWindow time.Duration Burst int CacheSize int + Clock clockwork.Clock } func NewRateLimiter(params RateLimitParams) (*RateLimiter, error) { @@ -35,12 +38,19 @@ func NewRateLimiter(params RateLimitParams) (*RateLimiter, error) { return nil, err } - return &RateLimiter{ + self := &RateLimiter{ rateLimit: params.RateLimit, timeWindow: params.TimeWindow, burst: params.Burst, cache: cache, - }, nil + clock: params.Clock, + } + + if self.clock == nil { + self.clock = clockwork.NewRealClock() + } + + return self, nil } func (rl *RateLimiter) Allow(r *http.Request) bool { @@ -51,7 +61,7 @@ func (rl *RateLimiter) Allow(r *http.Request) bool { return false } - return rl.getLimiter(r).Allow() + return rl.getLimiter(r).AllowN(rl.clock.Now(), 1) } func (rl *RateLimiter) getLimiter(r *http.Request) *rate.Limiter { diff --git a/internal/proxy/rate_limiter_test.go b/internal/proxy/rate_limiter_test.go index 7be418da..bf74ff11 100644 --- a/internal/proxy/rate_limiter_test.go +++ b/internal/proxy/rate_limiter_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/jonboulle/clockwork" "github.com/maxatome/go-testdeep" ) @@ -39,7 +40,7 @@ func TestMaxRequestsPerSec(t *testing.T) { name: "should limit the amount of requests per second", rateLimit: 10, - timeWindow: 1000, + timeWindow: time.Second, testTime: time.Second, reqSpecs: []reqSpec{ @@ -53,7 +54,7 @@ func TestMaxRequestsPerSec(t *testing.T) { name: "should restart the timer for the next time window", rateLimit: 10, - timeWindow: 500, + timeWindow: 500 * time.Millisecond, testTime: time.Second, reqSpecs: []reqSpec{ @@ -67,7 +68,7 @@ func TestMaxRequestsPerSec(t *testing.T) { name: "requests from different IPs should NOT influence each other", rateLimit: 10, - timeWindow: 1000, + timeWindow: time.Second, testTime: time.Second, reqSpecs: []reqSpec{ @@ -85,7 +86,7 @@ func TestMaxRequestsPerSec(t *testing.T) { name: "canceled request should always fail", rateLimit: 10, - timeWindow: 1000, + timeWindow: time.Second, testTime: time.Second, reqSpecs: []reqSpec{ @@ -98,30 +99,34 @@ func TestMaxRequestsPerSec(t *testing.T) { } for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() // Preparations + startTime := time.Now() + clock := clockwork.NewFakeClockAt(startTime) limiter, err := NewRateLimiter(RateLimitParams{ RateLimit: tt.rateLimit, TimeWindow: tt.timeWindow, Burst: 1, CacheSize: 100, + Clock: clock, }) testdeep.CmpNoError(t, err) - startTime := time.Now() successCounters := make([]int, len(tt.reqSpecs)) reqCounters := make([]int, len(tt.reqSpecs)) // The test itself - for time.Since(startTime) < tt.testTime { + for clock.Since(startTime) < tt.testTime { for idx, spec := range tt.reqSpecs { reqCounters[idx]++ if limiter.Allow(spec.req) { successCounters[idx]++ } } + clock.Advance(time.Millisecond) } // Check the expectations diff --git a/vendor/modules.txt b/vendor/modules.txt index ad463b15..73f36a15 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -70,6 +70,9 @@ github.com/hashicorp/golang-lru/v2/simplelru # github.com/jmespath/go-jmespath v0.4.0 ## explicit; go 1.14 github.com/jmespath/go-jmespath +# github.com/jonboulle/clockwork v0.4.0 +## explicit; go 1.15 +github.com/jonboulle/clockwork # github.com/kardianos/minwinsvc v1.0.0 ## explicit; go 1.15 github.com/kardianos/minwinsvc From b75326a0843ce0129abd8b3ed48d34dd6a578a4d Mon Sep 17 00:00:00 2001 From: Denis Krivosheev Date: Mon, 3 Apr 2023 10:49:28 +0300 Subject: [PATCH 10/13] Add vendor libs --- .../hashicorp/golang-lru/v2/.gitignore | 23 + .../hashicorp/golang-lru/v2/.golangci.yml | 30 ++ .../github.com/hashicorp/golang-lru/v2/2q.go | 223 +++++++++ .../hashicorp/golang-lru/v2/LICENSE | 364 +++++++++++++++ .../hashicorp/golang-lru/v2/README.md | 25 + .../github.com/hashicorp/golang-lru/v2/arc.go | 258 +++++++++++ .../github.com/hashicorp/golang-lru/v2/doc.go | 21 + .../github.com/hashicorp/golang-lru/v2/lru.go | 239 ++++++++++ .../golang-lru/v2/simplelru/LICENSE_list | 29 ++ .../hashicorp/golang-lru/v2/simplelru/list.go | 128 ++++++ .../hashicorp/golang-lru/v2/simplelru/lru.go | 164 +++++++ .../golang-lru/v2/simplelru/lru_interface.go | 40 ++ .../hashicorp/golang-lru/v2/testing.go | 16 + .../jonboulle/clockwork/.editorconfig | 12 + .../github.com/jonboulle/clockwork/.gitignore | 27 ++ vendor/github.com/jonboulle/clockwork/LICENSE | 201 ++++++++ .../github.com/jonboulle/clockwork/README.md | 80 ++++ .../jonboulle/clockwork/clockwork.go | 349 ++++++++++++++ .../github.com/jonboulle/clockwork/context.go | 25 + .../github.com/jonboulle/clockwork/ticker.go | 48 ++ .../github.com/jonboulle/clockwork/timer.go | 53 +++ vendor/golang.org/x/time/LICENSE | 27 ++ vendor/golang.org/x/time/PATENTS | 22 + vendor/golang.org/x/time/rate/rate.go | 428 ++++++++++++++++++ vendor/golang.org/x/time/rate/sometimes.go | 67 +++ 25 files changed, 2899 insertions(+) create mode 100644 vendor/github.com/hashicorp/golang-lru/v2/.gitignore create mode 100644 vendor/github.com/hashicorp/golang-lru/v2/.golangci.yml create mode 100644 vendor/github.com/hashicorp/golang-lru/v2/2q.go create mode 100644 vendor/github.com/hashicorp/golang-lru/v2/LICENSE create mode 100644 vendor/github.com/hashicorp/golang-lru/v2/README.md create mode 100644 vendor/github.com/hashicorp/golang-lru/v2/arc.go create mode 100644 vendor/github.com/hashicorp/golang-lru/v2/doc.go create mode 100644 vendor/github.com/hashicorp/golang-lru/v2/lru.go create mode 100644 vendor/github.com/hashicorp/golang-lru/v2/simplelru/LICENSE_list create mode 100644 vendor/github.com/hashicorp/golang-lru/v2/simplelru/list.go create mode 100644 vendor/github.com/hashicorp/golang-lru/v2/simplelru/lru.go create mode 100644 vendor/github.com/hashicorp/golang-lru/v2/simplelru/lru_interface.go create mode 100644 vendor/github.com/hashicorp/golang-lru/v2/testing.go create mode 100644 vendor/github.com/jonboulle/clockwork/.editorconfig create mode 100644 vendor/github.com/jonboulle/clockwork/.gitignore create mode 100644 vendor/github.com/jonboulle/clockwork/LICENSE create mode 100644 vendor/github.com/jonboulle/clockwork/README.md create mode 100644 vendor/github.com/jonboulle/clockwork/clockwork.go create mode 100644 vendor/github.com/jonboulle/clockwork/context.go create mode 100644 vendor/github.com/jonboulle/clockwork/ticker.go create mode 100644 vendor/github.com/jonboulle/clockwork/timer.go create mode 100644 vendor/golang.org/x/time/LICENSE create mode 100644 vendor/golang.org/x/time/PATENTS create mode 100644 vendor/golang.org/x/time/rate/rate.go create mode 100644 vendor/golang.org/x/time/rate/sometimes.go diff --git a/vendor/github.com/hashicorp/golang-lru/v2/.gitignore b/vendor/github.com/hashicorp/golang-lru/v2/.gitignore new file mode 100644 index 00000000..83656241 --- /dev/null +++ b/vendor/github.com/hashicorp/golang-lru/v2/.gitignore @@ -0,0 +1,23 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test diff --git a/vendor/github.com/hashicorp/golang-lru/v2/.golangci.yml b/vendor/github.com/hashicorp/golang-lru/v2/.golangci.yml new file mode 100644 index 00000000..49202fc4 --- /dev/null +++ b/vendor/github.com/hashicorp/golang-lru/v2/.golangci.yml @@ -0,0 +1,30 @@ +linters: + enable: + - megacheck + - revive + - govet + - unconvert + - megacheck + - gas + - gocyclo + - dupl + - misspell + - unparam + - unused + - typecheck + - ineffassign + - stylecheck + - exportloopref + - gocritic + - nakedret + - gosimple + - prealloc + fast: false + disable-all: true + +issues: + exclude-rules: + - path: _test\.go + linters: + - dupl + exclude-use-default: false diff --git a/vendor/github.com/hashicorp/golang-lru/v2/2q.go b/vendor/github.com/hashicorp/golang-lru/v2/2q.go new file mode 100644 index 00000000..0ad586bf --- /dev/null +++ b/vendor/github.com/hashicorp/golang-lru/v2/2q.go @@ -0,0 +1,223 @@ +package lru + +import ( + "fmt" + "sync" + + "github.com/hashicorp/golang-lru/v2/simplelru" +) + +const ( + // Default2QRecentRatio is the ratio of the 2Q cache dedicated + // to recently added entries that have only been accessed once. + Default2QRecentRatio = 0.25 + + // Default2QGhostEntries is the default ratio of ghost + // entries kept to track entries recently evicted + Default2QGhostEntries = 0.50 +) + +// TwoQueueCache is a thread-safe fixed size 2Q cache. +// 2Q is an enhancement over the standard LRU cache +// in that it tracks both frequently and recently used +// entries separately. This avoids a burst in access to new +// entries from evicting frequently used entries. It adds some +// additional tracking overhead to the standard LRU cache, and is +// computationally about 2x the cost, and adds some metadata over +// head. The ARCCache is similar, but does not require setting any +// parameters. +type TwoQueueCache[K comparable, V any] struct { + size int + recentSize int + + recent simplelru.LRUCache[K, V] + frequent simplelru.LRUCache[K, V] + recentEvict simplelru.LRUCache[K, V] + lock sync.RWMutex +} + +// New2Q creates a new TwoQueueCache using the default +// values for the parameters. +func New2Q[K comparable, V any](size int) (*TwoQueueCache[K, V], error) { + return New2QParams[K, V](size, Default2QRecentRatio, Default2QGhostEntries) +} + +// New2QParams creates a new TwoQueueCache using the provided +// parameter values. +func New2QParams[K comparable, V any](size int, recentRatio, ghostRatio float64) (*TwoQueueCache[K, V], error) { + if size <= 0 { + return nil, fmt.Errorf("invalid size") + } + if recentRatio < 0.0 || recentRatio > 1.0 { + return nil, fmt.Errorf("invalid recent ratio") + } + if ghostRatio < 0.0 || ghostRatio > 1.0 { + return nil, fmt.Errorf("invalid ghost ratio") + } + + // Determine the sub-sizes + recentSize := int(float64(size) * recentRatio) + evictSize := int(float64(size) * ghostRatio) + + // Allocate the LRUs + recent, err := simplelru.NewLRU[K, V](size, nil) + if err != nil { + return nil, err + } + frequent, err := simplelru.NewLRU[K, V](size, nil) + if err != nil { + return nil, err + } + recentEvict, err := simplelru.NewLRU[K, V](evictSize, nil) + if err != nil { + return nil, err + } + + // Initialize the cache + c := &TwoQueueCache[K, V]{ + size: size, + recentSize: recentSize, + recent: recent, + frequent: frequent, + recentEvict: recentEvict, + } + return c, nil +} + +// Get looks up a key's value from the cache. +func (c *TwoQueueCache[K, V]) Get(key K) (value V, ok bool) { + c.lock.Lock() + defer c.lock.Unlock() + + // Check if this is a frequent value + if val, ok := c.frequent.Get(key); ok { + return val, ok + } + + // If the value is contained in recent, then we + // promote it to frequent + if val, ok := c.recent.Peek(key); ok { + c.recent.Remove(key) + c.frequent.Add(key, val) + return val, ok + } + + // No hit + return +} + +// Add adds a value to the cache. +func (c *TwoQueueCache[K, V]) Add(key K, value V) { + c.lock.Lock() + defer c.lock.Unlock() + + // Check if the value is frequently used already, + // and just update the value + if c.frequent.Contains(key) { + c.frequent.Add(key, value) + return + } + + // Check if the value is recently used, and promote + // the value into the frequent list + if c.recent.Contains(key) { + c.recent.Remove(key) + c.frequent.Add(key, value) + return + } + + // If the value was recently evicted, add it to the + // frequently used list + if c.recentEvict.Contains(key) { + c.ensureSpace(true) + c.recentEvict.Remove(key) + c.frequent.Add(key, value) + return + } + + // Add to the recently seen list + c.ensureSpace(false) + c.recent.Add(key, value) +} + +// ensureSpace is used to ensure we have space in the cache +func (c *TwoQueueCache[K, V]) ensureSpace(recentEvict bool) { + // If we have space, nothing to do + recentLen := c.recent.Len() + freqLen := c.frequent.Len() + if recentLen+freqLen < c.size { + return + } + + // If the recent buffer is larger than + // the target, evict from there + if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) { + k, _, _ := c.recent.RemoveOldest() + var empty V + c.recentEvict.Add(k, empty) + return + } + + // Remove from the frequent list otherwise + c.frequent.RemoveOldest() +} + +// Len returns the number of items in the cache. +func (c *TwoQueueCache[K, V]) Len() int { + c.lock.RLock() + defer c.lock.RUnlock() + return c.recent.Len() + c.frequent.Len() +} + +// Keys returns a slice of the keys in the cache. +// The frequently used keys are first in the returned slice. +func (c *TwoQueueCache[K, V]) Keys() []K { + c.lock.RLock() + defer c.lock.RUnlock() + k1 := c.frequent.Keys() + k2 := c.recent.Keys() + return append(k1, k2...) +} + +// Remove removes the provided key from the cache. +func (c *TwoQueueCache[K, V]) Remove(key K) { + c.lock.Lock() + defer c.lock.Unlock() + if c.frequent.Remove(key) { + return + } + if c.recent.Remove(key) { + return + } + if c.recentEvict.Remove(key) { + return + } +} + +// Purge is used to completely clear the cache. +func (c *TwoQueueCache[K, V]) Purge() { + c.lock.Lock() + defer c.lock.Unlock() + c.recent.Purge() + c.frequent.Purge() + c.recentEvict.Purge() +} + +// Contains is used to check if the cache contains a key +// without updating recency or frequency. +func (c *TwoQueueCache[K, V]) Contains(key K) bool { + c.lock.RLock() + defer c.lock.RUnlock() + return c.frequent.Contains(key) || c.recent.Contains(key) +} + +// Peek is used to inspect the cache value of a key +// without updating recency or frequency. +func (c *TwoQueueCache[K, V]) Peek(key K) (value V, ok bool) { + c.lock.RLock() + defer c.lock.RUnlock() + if val, ok := c.frequent.Peek(key); ok { + return val, ok + } + return c.recent.Peek(key) +} diff --git a/vendor/github.com/hashicorp/golang-lru/v2/LICENSE b/vendor/github.com/hashicorp/golang-lru/v2/LICENSE new file mode 100644 index 00000000..0e5d580e --- /dev/null +++ b/vendor/github.com/hashicorp/golang-lru/v2/LICENSE @@ -0,0 +1,364 @@ +Copyright (c) 2014 HashiCorp, Inc. + +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. "Contributor" + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. "Contributor Version" + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the terms of + a Secondary License. + +1.6. "Executable Form" + + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + + means a work that combines Covered Software with other material, in a + separate file or files, that is not Covered Software. + +1.8. "License" + + means this document. + +1.9. "Licensable" + + means having the right to grant, to the maximum extent possible, whether + at the time of the initial grant or subsequently, any and all of the + rights conveyed by this License. + +1.10. "Modifications" + + means any of the following: + + a. any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. "Patent Claims" of a Contributor + + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the License, + by the making, using, selling, offering for sale, having made, import, + or transfer of either its Contributions or its Contributor Version. + +1.12. "Secondary License" + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. "Source Code Form" + + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, "control" means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution + become effective for each Contribution on the date the Contributor first + distributes such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under + this License. No additional rights or licenses will be implied from the + distribution or licensing of Covered Software under this License. + Notwithstanding Section 2.1(b) above, no patent license is granted by a + Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of + its Contributions. + + This License does not grant any rights in the trademarks, service marks, + or logos of any Contributor (except as may be necessary to comply with + the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this + License (see Section 10.2) or under the terms of a Secondary License (if + permitted under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its + Contributions are its original creation(s) or it has sufficient rights to + grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under + applicable copyright doctrines of fair use, fair dealing, or other + equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under + the terms of this License. You must inform recipients that the Source + Code Form of the Covered Software is governed by the terms of this + License, and how they can obtain a copy of this License. You may not + attempt to alter or restrict the recipients' rights in the Source Code + Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter the + recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for + the Covered Software. If the Larger Work is a combination of Covered + Software with a work governed by one or more Secondary Licenses, and the + Covered Software is not Incompatible With Secondary Licenses, this + License permits You to additionally distribute such Covered Software + under the terms of such Secondary License(s), so that the recipient of + the Larger Work may, at their option, further distribute the Covered + Software under the terms of either this License or such Secondary + License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices + (including copyright notices, patent notices, disclaimers of warranty, or + limitations of liability) contained within the Source Code Form of the + Covered Software, except that You may alter any license notices to the + extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on + behalf of any Contributor. You must make it absolutely clear that any + such warranty, support, indemnity, or liability obligation is offered by + You alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, + judicial order, or regulation then You must: (a) comply with the terms of + this License to the maximum extent possible; and (b) describe the + limitations and the code they affect. Such description must be placed in a + text file included with all distributions of the Covered Software under + this License. Except to the extent prohibited by statute or regulation, + such description must be sufficiently detailed for a recipient of ordinary + skill to be able to understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing + basis, if such Contributor fails to notify You of the non-compliance by + some reasonable means prior to 60 days after You have come back into + compliance. Moreover, Your grants from a particular Contributor are + reinstated on an ongoing basis if such Contributor notifies You of the + non-compliance by some reasonable means, this is the first time You have + received notice of non-compliance with this License from such + Contributor, and You become compliant prior to 30 days after Your receipt + of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, + counter-claims, and cross-claims) alleging that a Contributor Version + directly or indirectly infringes any patent, then the rights granted to + You by any and all Contributors for the Covered Software under Section + 2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an "as is" basis, + without warranty of any kind, either expressed, implied, or statutory, + including, without limitation, warranties that the Covered Software is free + of defects, merchantable, fit for a particular purpose or non-infringing. + The entire risk as to the quality and performance of the Covered Software + is with You. Should any Covered Software prove defective in any respect, + You (not any Contributor) assume the cost of any necessary servicing, + repair, or correction. This disclaimer of warranty constitutes an essential + part of this License. No use of any Covered Software is authorized under + this License except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from + such party's negligence to the extent applicable law prohibits such + limitation. Some jurisdictions do not allow the exclusion or limitation of + incidental or consequential damages, so this exclusion and limitation may + not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts + of a jurisdiction where the defendant maintains its principal place of + business and such litigation shall be governed by laws of that + jurisdiction, without reference to its conflict-of-law provisions. Nothing + in this Section shall prevent a party's ability to bring cross-claims or + counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. Any law or regulation which provides that + the language of a contract shall be construed against the drafter shall not + be used to construe this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version + of the License under which You originally received the Covered Software, + or under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a + modified version of this License if you rename the license and remove + any references to the name of the license steward (except to note that + such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary + Licenses If You choose to distribute Source Code Form that is + Incompatible With Secondary Licenses under the terms of this version of + the License, the notice described in Exhibit B of this License must be + attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, +then You may include the notice in a location (such as a LICENSE file in a +relevant directory) where a recipient would be likely to look for such a +notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice + + This Source Code Form is "Incompatible + With Secondary Licenses", as defined by + the Mozilla Public License, v. 2.0. diff --git a/vendor/github.com/hashicorp/golang-lru/v2/README.md b/vendor/github.com/hashicorp/golang-lru/v2/README.md new file mode 100644 index 00000000..bd14098e --- /dev/null +++ b/vendor/github.com/hashicorp/golang-lru/v2/README.md @@ -0,0 +1,25 @@ +golang-lru +========== + +This provides the `lru` package which implements a fixed-size +thread safe LRU cache. It is based on the cache in Groupcache. + +Documentation +============= + +Full docs are available on [Go Packages](https://pkg.go.dev/github.com/hashicorp/golang-lru/v2) + +Example +======= + +Using the LRU is very simple: + +```go +l, _ := New(128) +for i := 0; i < 256; i++ { + l.Add(i, nil) +} +if l.Len() != 128 { + panic(fmt.Sprintf("bad len: %v", l.Len())) +} +``` diff --git a/vendor/github.com/hashicorp/golang-lru/v2/arc.go b/vendor/github.com/hashicorp/golang-lru/v2/arc.go new file mode 100644 index 00000000..8733890c --- /dev/null +++ b/vendor/github.com/hashicorp/golang-lru/v2/arc.go @@ -0,0 +1,258 @@ +package lru + +import ( + "sync" + + "github.com/hashicorp/golang-lru/v2/simplelru" +) + +// ARCCache is a thread-safe fixed size Adaptive Replacement Cache (ARC). +// ARC is an enhancement over the standard LRU cache in that tracks both +// frequency and recency of use. This avoids a burst in access to new +// entries from evicting the frequently used older entries. It adds some +// additional tracking overhead to a standard LRU cache, computationally +// it is roughly 2x the cost, and the extra memory overhead is linear +// with the size of the cache. ARC has been patented by IBM, but is +// similar to the TwoQueueCache (2Q) which requires setting parameters. +type ARCCache[K comparable, V any] struct { + size int // Size is the total capacity of the cache + p int // P is the dynamic preference towards T1 or T2 + + t1 simplelru.LRUCache[K, V] // T1 is the LRU for recently accessed items + b1 simplelru.LRUCache[K, V] // B1 is the LRU for evictions from t1 + + t2 simplelru.LRUCache[K, V] // T2 is the LRU for frequently accessed items + b2 simplelru.LRUCache[K, V] // B2 is the LRU for evictions from t2 + + lock sync.RWMutex +} + +// NewARC creates an ARC of the given size +func NewARC[K comparable, V any](size int) (*ARCCache[K, V], error) { + // Create the sub LRUs + b1, err := simplelru.NewLRU[K, V](size, nil) + if err != nil { + return nil, err + } + b2, err := simplelru.NewLRU[K, V](size, nil) + if err != nil { + return nil, err + } + t1, err := simplelru.NewLRU[K, V](size, nil) + if err != nil { + return nil, err + } + t2, err := simplelru.NewLRU[K, V](size, nil) + if err != nil { + return nil, err + } + + // Initialize the ARC + c := &ARCCache[K, V]{ + size: size, + p: 0, + t1: t1, + b1: b1, + t2: t2, + b2: b2, + } + return c, nil +} + +// Get looks up a key's value from the cache. +func (c *ARCCache[K, V]) Get(key K) (value V, ok bool) { + c.lock.Lock() + defer c.lock.Unlock() + + // If the value is contained in T1 (recent), then + // promote it to T2 (frequent) + if val, ok := c.t1.Peek(key); ok { + c.t1.Remove(key) + c.t2.Add(key, val) + return val, ok + } + + // Check if the value is contained in T2 (frequent) + if val, ok := c.t2.Get(key); ok { + return val, ok + } + + // No hit + return +} + +// Add adds a value to the cache. +func (c *ARCCache[K, V]) Add(key K, value V) { + c.lock.Lock() + defer c.lock.Unlock() + + // Check if the value is contained in T1 (recent), and potentially + // promote it to frequent T2 + if c.t1.Contains(key) { + c.t1.Remove(key) + c.t2.Add(key, value) + return + } + + // Check if the value is already in T2 (frequent) and update it + if c.t2.Contains(key) { + c.t2.Add(key, value) + return + } + + // Check if this value was recently evicted as part of the + // recently used list + if c.b1.Contains(key) { + // T1 set is too small, increase P appropriately + delta := 1 + b1Len := c.b1.Len() + b2Len := c.b2.Len() + if b2Len > b1Len { + delta = b2Len / b1Len + } + if c.p+delta >= c.size { + c.p = c.size + } else { + c.p += delta + } + + // Potentially need to make room in the cache + if c.t1.Len()+c.t2.Len() >= c.size { + c.replace(false) + } + + // Remove from B1 + c.b1.Remove(key) + + // Add the key to the frequently used list + c.t2.Add(key, value) + return + } + + // Check if this value was recently evicted as part of the + // frequently used list + if c.b2.Contains(key) { + // T2 set is too small, decrease P appropriately + delta := 1 + b1Len := c.b1.Len() + b2Len := c.b2.Len() + if b1Len > b2Len { + delta = b1Len / b2Len + } + if delta >= c.p { + c.p = 0 + } else { + c.p -= delta + } + + // Potentially need to make room in the cache + if c.t1.Len()+c.t2.Len() >= c.size { + c.replace(true) + } + + // Remove from B2 + c.b2.Remove(key) + + // Add the key to the frequently used list + c.t2.Add(key, value) + return + } + + // Potentially need to make room in the cache + if c.t1.Len()+c.t2.Len() >= c.size { + c.replace(false) + } + + // Keep the size of the ghost buffers trim + if c.b1.Len() > c.size-c.p { + c.b1.RemoveOldest() + } + if c.b2.Len() > c.p { + c.b2.RemoveOldest() + } + + // Add to the recently seen list + c.t1.Add(key, value) +} + +// replace is used to adaptively evict from either T1 or T2 +// based on the current learned value of P +func (c *ARCCache[K, V]) replace(b2ContainsKey bool) { + t1Len := c.t1.Len() + if t1Len > 0 && (t1Len > c.p || (t1Len == c.p && b2ContainsKey)) { + k, _, ok := c.t1.RemoveOldest() + if ok { + var empty V + c.b1.Add(k, empty) + } + } else { + k, _, ok := c.t2.RemoveOldest() + if ok { + var empty V + c.b2.Add(k, empty) + } + } +} + +// Len returns the number of cached entries +func (c *ARCCache[K, V]) Len() int { + c.lock.RLock() + defer c.lock.RUnlock() + return c.t1.Len() + c.t2.Len() +} + +// Keys returns all the cached keys +func (c *ARCCache[K, V]) Keys() []K { + c.lock.RLock() + defer c.lock.RUnlock() + k1 := c.t1.Keys() + k2 := c.t2.Keys() + return append(k1, k2...) +} + +// Remove is used to purge a key from the cache +func (c *ARCCache[K, V]) Remove(key K) { + c.lock.Lock() + defer c.lock.Unlock() + if c.t1.Remove(key) { + return + } + if c.t2.Remove(key) { + return + } + if c.b1.Remove(key) { + return + } + if c.b2.Remove(key) { + return + } +} + +// Purge is used to clear the cache +func (c *ARCCache[K, V]) Purge() { + c.lock.Lock() + defer c.lock.Unlock() + c.t1.Purge() + c.t2.Purge() + c.b1.Purge() + c.b2.Purge() +} + +// Contains is used to check if the cache contains a key +// without updating recency or frequency. +func (c *ARCCache[K, V]) Contains(key K) bool { + c.lock.RLock() + defer c.lock.RUnlock() + return c.t1.Contains(key) || c.t2.Contains(key) +} + +// Peek is used to inspect the cache value of a key +// without updating recency or frequency. +func (c *ARCCache[K, V]) Peek(key K) (value V, ok bool) { + c.lock.RLock() + defer c.lock.RUnlock() + if val, ok := c.t1.Peek(key); ok { + return val, ok + } + return c.t2.Peek(key) +} diff --git a/vendor/github.com/hashicorp/golang-lru/v2/doc.go b/vendor/github.com/hashicorp/golang-lru/v2/doc.go new file mode 100644 index 00000000..2547df97 --- /dev/null +++ b/vendor/github.com/hashicorp/golang-lru/v2/doc.go @@ -0,0 +1,21 @@ +// Package lru provides three different LRU caches of varying sophistication. +// +// Cache is a simple LRU cache. It is based on the +// LRU implementation in groupcache: +// https://github.com/golang/groupcache/tree/master/lru +// +// TwoQueueCache tracks frequently used and recently used entries separately. +// This avoids a burst of accesses from taking out frequently used entries, +// at the cost of about 2x computational overhead and some extra bookkeeping. +// +// ARCCache is an adaptive replacement cache. It tracks recent evictions as +// well as recent usage in both the frequent and recent caches. Its +// computational overhead is comparable to TwoQueueCache, but the memory +// overhead is linear with the size of the cache. +// +// ARC has been patented by IBM, so do not use it if that is problematic for +// your program. +// +// All caches in this package take locks while operating, and are therefore +// thread-safe for consumers. +package lru diff --git a/vendor/github.com/hashicorp/golang-lru/v2/lru.go b/vendor/github.com/hashicorp/golang-lru/v2/lru.go new file mode 100644 index 00000000..efa305f7 --- /dev/null +++ b/vendor/github.com/hashicorp/golang-lru/v2/lru.go @@ -0,0 +1,239 @@ +package lru + +import ( + "sync" + + "github.com/hashicorp/golang-lru/v2/simplelru" +) + +const ( + // DefaultEvictedBufferSize defines the default buffer size to store evicted key/val + DefaultEvictedBufferSize = 16 +) + +// Cache is a thread-safe fixed size LRU cache. +type Cache[K comparable, V any] struct { + lru *simplelru.LRU[K, V] + evictedKeys []K + evictedVals []V + onEvictedCB func(k K, v V) + lock sync.RWMutex +} + +// New creates an LRU of the given size. +func New[K comparable, V any](size int) (*Cache[K, V], error) { + return NewWithEvict[K, V](size, nil) +} + +// NewWithEvict constructs a fixed size cache with the given eviction +// callback. +func NewWithEvict[K comparable, V any](size int, onEvicted func(key K, value V)) (c *Cache[K, V], err error) { + // create a cache with default settings + c = &Cache[K, V]{ + onEvictedCB: onEvicted, + } + if onEvicted != nil { + c.initEvictBuffers() + onEvicted = c.onEvicted + } + c.lru, err = simplelru.NewLRU(size, onEvicted) + return +} + +func (c *Cache[K, V]) initEvictBuffers() { + c.evictedKeys = make([]K, 0, DefaultEvictedBufferSize) + c.evictedVals = make([]V, 0, DefaultEvictedBufferSize) +} + +// onEvicted save evicted key/val and sent in externally registered callback +// outside of critical section +func (c *Cache[K, V]) onEvicted(k K, v V) { + c.evictedKeys = append(c.evictedKeys, k) + c.evictedVals = append(c.evictedVals, v) +} + +// Purge is used to completely clear the cache. +func (c *Cache[K, V]) Purge() { + var ks []K + var vs []V + c.lock.Lock() + c.lru.Purge() + if c.onEvictedCB != nil && len(c.evictedKeys) > 0 { + ks, vs = c.evictedKeys, c.evictedVals + c.initEvictBuffers() + } + c.lock.Unlock() + // invoke callback outside of critical section + if c.onEvictedCB != nil { + for i := 0; i < len(ks); i++ { + c.onEvictedCB(ks[i], vs[i]) + } + } +} + +// Add adds a value to the cache. Returns true if an eviction occurred. +func (c *Cache[K, V]) Add(key K, value V) (evicted bool) { + var k K + var v V + c.lock.Lock() + evicted = c.lru.Add(key, value) + if c.onEvictedCB != nil && evicted { + k, v = c.evictedKeys[0], c.evictedVals[0] + c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] + } + c.lock.Unlock() + if c.onEvictedCB != nil && evicted { + c.onEvictedCB(k, v) + } + return +} + +// Get looks up a key's value from the cache. +func (c *Cache[K, V]) Get(key K) (value V, ok bool) { + c.lock.Lock() + value, ok = c.lru.Get(key) + c.lock.Unlock() + return value, ok +} + +// Contains checks if a key is in the cache, without updating the +// recent-ness or deleting it for being stale. +func (c *Cache[K, V]) Contains(key K) bool { + c.lock.RLock() + containKey := c.lru.Contains(key) + c.lock.RUnlock() + return containKey +} + +// Peek returns the key value (or undefined if not found) without updating +// the "recently used"-ness of the key. +func (c *Cache[K, V]) Peek(key K) (value V, ok bool) { + c.lock.RLock() + value, ok = c.lru.Peek(key) + c.lock.RUnlock() + return value, ok +} + +// ContainsOrAdd checks if a key is in the cache without updating the +// recent-ness or deleting it for being stale, and if not, adds the value. +// Returns whether found and whether an eviction occurred. +func (c *Cache[K, V]) ContainsOrAdd(key K, value V) (ok, evicted bool) { + var k K + var v V + c.lock.Lock() + if c.lru.Contains(key) { + c.lock.Unlock() + return true, false + } + evicted = c.lru.Add(key, value) + if c.onEvictedCB != nil && evicted { + k, v = c.evictedKeys[0], c.evictedVals[0] + c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] + } + c.lock.Unlock() + if c.onEvictedCB != nil && evicted { + c.onEvictedCB(k, v) + } + return false, evicted +} + +// PeekOrAdd checks if a key is in the cache without updating the +// recent-ness or deleting it for being stale, and if not, adds the value. +// Returns whether found and whether an eviction occurred. +func (c *Cache[K, V]) PeekOrAdd(key K, value V) (previous V, ok, evicted bool) { + var k K + var v V + c.lock.Lock() + previous, ok = c.lru.Peek(key) + if ok { + c.lock.Unlock() + return previous, true, false + } + evicted = c.lru.Add(key, value) + if c.onEvictedCB != nil && evicted { + k, v = c.evictedKeys[0], c.evictedVals[0] + c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] + } + c.lock.Unlock() + if c.onEvictedCB != nil && evicted { + c.onEvictedCB(k, v) + } + return +} + +// Remove removes the provided key from the cache. +func (c *Cache[K, V]) Remove(key K) (present bool) { + var k K + var v V + c.lock.Lock() + present = c.lru.Remove(key) + if c.onEvictedCB != nil && present { + k, v = c.evictedKeys[0], c.evictedVals[0] + c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] + } + c.lock.Unlock() + if c.onEvictedCB != nil && present { + c.onEvictedCB(k, v) + } + return +} + +// Resize changes the cache size. +func (c *Cache[K, V]) Resize(size int) (evicted int) { + var ks []K + var vs []V + c.lock.Lock() + evicted = c.lru.Resize(size) + if c.onEvictedCB != nil && evicted > 0 { + ks, vs = c.evictedKeys, c.evictedVals + c.initEvictBuffers() + } + c.lock.Unlock() + if c.onEvictedCB != nil && evicted > 0 { + for i := 0; i < len(ks); i++ { + c.onEvictedCB(ks[i], vs[i]) + } + } + return evicted +} + +// RemoveOldest removes the oldest item from the cache. +func (c *Cache[K, V]) RemoveOldest() (key K, value V, ok bool) { + var k K + var v V + c.lock.Lock() + key, value, ok = c.lru.RemoveOldest() + if c.onEvictedCB != nil && ok { + k, v = c.evictedKeys[0], c.evictedVals[0] + c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] + } + c.lock.Unlock() + if c.onEvictedCB != nil && ok { + c.onEvictedCB(k, v) + } + return +} + +// GetOldest returns the oldest entry +func (c *Cache[K, V]) GetOldest() (key K, value V, ok bool) { + c.lock.RLock() + key, value, ok = c.lru.GetOldest() + c.lock.RUnlock() + return +} + +// Keys returns a slice of the keys in the cache, from oldest to newest. +func (c *Cache[K, V]) Keys() []K { + c.lock.RLock() + keys := c.lru.Keys() + c.lock.RUnlock() + return keys +} + +// Len returns the number of items in the cache. +func (c *Cache[K, V]) Len() int { + c.lock.RLock() + length := c.lru.Len() + c.lock.RUnlock() + return length +} diff --git a/vendor/github.com/hashicorp/golang-lru/v2/simplelru/LICENSE_list b/vendor/github.com/hashicorp/golang-lru/v2/simplelru/LICENSE_list new file mode 100644 index 00000000..c4764e6b --- /dev/null +++ b/vendor/github.com/hashicorp/golang-lru/v2/simplelru/LICENSE_list @@ -0,0 +1,29 @@ +This license applies to simplelru/list.go + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/hashicorp/golang-lru/v2/simplelru/list.go b/vendor/github.com/hashicorp/golang-lru/v2/simplelru/list.go new file mode 100644 index 00000000..c39da3c1 --- /dev/null +++ b/vendor/github.com/hashicorp/golang-lru/v2/simplelru/list.go @@ -0,0 +1,128 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE_list file. + +package simplelru + +// entry is an LRU entry +type entry[K comparable, V any] struct { + // Next and previous pointers in the doubly-linked list of elements. + // To simplify the implementation, internally a list l is implemented + // as a ring, such that &l.root is both the next element of the last + // list element (l.Back()) and the previous element of the first list + // element (l.Front()). + next, prev *entry[K, V] + + // The list to which this element belongs. + list *lruList[K, V] + + // The LRU key of this element. + key K + + // The value stored with this element. + value V +} + +// prevEntry returns the previous list element or nil. +func (e *entry[K, V]) prevEntry() *entry[K, V] { + if p := e.prev; e.list != nil && p != &e.list.root { + return p + } + return nil +} + +// lruList represents a doubly linked list. +// The zero value for lruList is an empty list ready to use. +type lruList[K comparable, V any] struct { + root entry[K, V] // sentinel list element, only &root, root.prev, and root.next are used + len int // current list length excluding (this) sentinel element +} + +// init initializes or clears list l. +func (l *lruList[K, V]) init() *lruList[K, V] { + l.root.next = &l.root + l.root.prev = &l.root + l.len = 0 + return l +} + +// newList returns an initialized list. +func newList[K comparable, V any]() *lruList[K, V] { return new(lruList[K, V]).init() } + +// length returns the number of elements of list l. +// The complexity is O(1). +func (l *lruList[K, V]) length() int { return l.len } + +// back returns the last element of list l or nil if the list is empty. +func (l *lruList[K, V]) back() *entry[K, V] { + if l.len == 0 { + return nil + } + return l.root.prev +} + +// lazyInit lazily initializes a zero List value. +func (l *lruList[K, V]) lazyInit() { + if l.root.next == nil { + l.init() + } +} + +// insert inserts e after at, increments l.len, and returns e. +func (l *lruList[K, V]) insert(e, at *entry[K, V]) *entry[K, V] { + e.prev = at + e.next = at.next + e.prev.next = e + e.next.prev = e + e.list = l + l.len++ + return e +} + +// insertValue is a convenience wrapper for insert(&Element{Value: v}, at). +func (l *lruList[K, V]) insertValue(k K, v V, at *entry[K, V]) *entry[K, V] { + return l.insert(&entry[K, V]{value: v, key: k}, at) +} + +// remove removes e from its list, decrements l.len +func (l *lruList[K, V]) remove(e *entry[K, V]) V { + e.prev.next = e.next + e.next.prev = e.prev + e.next = nil // avoid memory leaks + e.prev = nil // avoid memory leaks + e.list = nil + l.len-- + + return e.value +} + +// move moves e to next to at. +func (l *lruList[K, V]) move(e, at *entry[K, V]) { + if e == at { + return + } + e.prev.next = e.next + e.next.prev = e.prev + + e.prev = at + e.next = at.next + e.prev.next = e + e.next.prev = e +} + +// pushFront inserts a new element e with value v at the front of list l and returns e. +func (l *lruList[K, V]) pushFront(k K, v V) *entry[K, V] { + l.lazyInit() + return l.insertValue(k, v, &l.root) +} + +// moveToFront moves element e to the front of list l. +// If e is not an element of l, the list is not modified. +// The element must not be nil. +func (l *lruList[K, V]) moveToFront(e *entry[K, V]) { + if e.list != l || l.root.next == e { + return + } + // see comment in List.Remove about initialization of l + l.move(e, &l.root) +} diff --git a/vendor/github.com/hashicorp/golang-lru/v2/simplelru/lru.go b/vendor/github.com/hashicorp/golang-lru/v2/simplelru/lru.go new file mode 100644 index 00000000..e7a9ade0 --- /dev/null +++ b/vendor/github.com/hashicorp/golang-lru/v2/simplelru/lru.go @@ -0,0 +1,164 @@ +package simplelru + +import ( + "errors" +) + +// EvictCallback is used to get a callback when a cache entry is evicted +type EvictCallback[K comparable, V any] func(key K, value V) + +// LRU implements a non-thread safe fixed size LRU cache +type LRU[K comparable, V any] struct { + size int + evictList *lruList[K, V] + items map[K]*entry[K, V] + onEvict EvictCallback[K, V] +} + +// NewLRU constructs an LRU of the given size +func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, V], error) { + if size <= 0 { + return nil, errors.New("must provide a positive size") + } + + c := &LRU[K, V]{ + size: size, + evictList: newList[K, V](), + items: make(map[K]*entry[K, V]), + onEvict: onEvict, + } + return c, nil +} + +// Purge is used to completely clear the cache. +func (c *LRU[K, V]) Purge() { + for k, v := range c.items { + if c.onEvict != nil { + c.onEvict(k, v.value) + } + delete(c.items, k) + } + c.evictList.init() +} + +// Add adds a value to the cache. Returns true if an eviction occurred. +func (c *LRU[K, V]) Add(key K, value V) (evicted bool) { + // Check for existing item + if ent, ok := c.items[key]; ok { + c.evictList.moveToFront(ent) + ent.value = value + return false + } + + // Add new item + ent := c.evictList.pushFront(key, value) + c.items[key] = ent + + evict := c.evictList.length() > c.size + // Verify size not exceeded + if evict { + c.removeOldest() + } + return evict +} + +// Get looks up a key's value from the cache. +func (c *LRU[K, V]) Get(key K) (value V, ok bool) { + if ent, ok := c.items[key]; ok { + c.evictList.moveToFront(ent) + return ent.value, true + } + return +} + +// Contains checks if a key is in the cache, without updating the recent-ness +// or deleting it for being stale. +func (c *LRU[K, V]) Contains(key K) (ok bool) { + _, ok = c.items[key] + return ok +} + +// Peek returns the key value (or undefined if not found) without updating +// the "recently used"-ness of the key. +func (c *LRU[K, V]) Peek(key K) (value V, ok bool) { + var ent *entry[K, V] + if ent, ok = c.items[key]; ok { + return ent.value, true + } + return +} + +// Remove removes the provided key from the cache, returning if the +// key was contained. +func (c *LRU[K, V]) Remove(key K) (present bool) { + if ent, ok := c.items[key]; ok { + c.removeElement(ent) + return true + } + return false +} + +// RemoveOldest removes the oldest item from the cache. +func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) { + ent := c.evictList.back() + if ent != nil { + c.removeElement(ent) + return ent.key, ent.value, true + } + return +} + +// GetOldest returns the oldest entry +func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) { + ent := c.evictList.back() + if ent != nil { + return ent.key, ent.value, true + } + return +} + +// Keys returns a slice of the keys in the cache, from oldest to newest. +func (c *LRU[K, V]) Keys() []K { + keys := make([]K, c.evictList.length()) + i := 0 + for ent := c.evictList.back(); ent != nil; ent = ent.prevEntry() { + keys[i] = ent.key + i++ + } + return keys +} + +// Len returns the number of items in the cache. +func (c *LRU[K, V]) Len() int { + return c.evictList.length() +} + +// Resize changes the cache size. +func (c *LRU[K, V]) Resize(size int) (evicted int) { + diff := c.Len() - size + if diff < 0 { + diff = 0 + } + for i := 0; i < diff; i++ { + c.removeOldest() + } + c.size = size + return diff +} + +// removeOldest removes the oldest item from the cache. +func (c *LRU[K, V]) removeOldest() { + ent := c.evictList.back() + if ent != nil { + c.removeElement(ent) + } +} + +// removeElement is used to remove a given list element from the cache +func (c *LRU[K, V]) removeElement(e *entry[K, V]) { + c.evictList.remove(e) + delete(c.items, e.key) + if c.onEvict != nil { + c.onEvict(e.key, e.value) + } +} diff --git a/vendor/github.com/hashicorp/golang-lru/v2/simplelru/lru_interface.go b/vendor/github.com/hashicorp/golang-lru/v2/simplelru/lru_interface.go new file mode 100644 index 00000000..aa8edfb0 --- /dev/null +++ b/vendor/github.com/hashicorp/golang-lru/v2/simplelru/lru_interface.go @@ -0,0 +1,40 @@ +// Package simplelru provides simple LRU implementation based on build-in container/list. +package simplelru + +// LRUCache is the interface for simple LRU cache. +type LRUCache[K comparable, V any] interface { + // Adds a value to the cache, returns true if an eviction occurred and + // updates the "recently used"-ness of the key. + Add(key K, value V) bool + + // Returns key's value from the cache and + // updates the "recently used"-ness of the key. #value, isFound + Get(key K) (value V, ok bool) + + // Checks if a key exists in cache without updating the recent-ness. + Contains(key K) (ok bool) + + // Returns key's value without updating the "recently used"-ness of the key. + Peek(key K) (value V, ok bool) + + // Removes a key from the cache. + Remove(key K) bool + + // Removes the oldest entry from cache. + RemoveOldest() (K, V, bool) + + // Returns the oldest entry from the cache. #key, value, isFound + GetOldest() (K, V, bool) + + // Returns a slice of the keys in the cache, from oldest to newest. + Keys() []K + + // Returns the number of items in the cache. + Len() int + + // Clears all cache entries. + Purge() + + // Resizes cache, returning number evicted + Resize(int) int +} diff --git a/vendor/github.com/hashicorp/golang-lru/v2/testing.go b/vendor/github.com/hashicorp/golang-lru/v2/testing.go new file mode 100644 index 00000000..49276078 --- /dev/null +++ b/vendor/github.com/hashicorp/golang-lru/v2/testing.go @@ -0,0 +1,16 @@ +package lru + +import ( + "crypto/rand" + "math" + "math/big" + "testing" +) + +func getRand(tb testing.TB) int64 { + out, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + tb.Fatal(err) + } + return out.Int64() +} diff --git a/vendor/github.com/jonboulle/clockwork/.editorconfig b/vendor/github.com/jonboulle/clockwork/.editorconfig new file mode 100644 index 00000000..4492e9f9 --- /dev/null +++ b/vendor/github.com/jonboulle/clockwork/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.go] +indent_style = tab diff --git a/vendor/github.com/jonboulle/clockwork/.gitignore b/vendor/github.com/jonboulle/clockwork/.gitignore new file mode 100644 index 00000000..00852bd9 --- /dev/null +++ b/vendor/github.com/jonboulle/clockwork/.gitignore @@ -0,0 +1,27 @@ +/.idea/ + +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test + +*.swp diff --git a/vendor/github.com/jonboulle/clockwork/LICENSE b/vendor/github.com/jonboulle/clockwork/LICENSE new file mode 100644 index 00000000..5c304d1a --- /dev/null +++ b/vendor/github.com/jonboulle/clockwork/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/vendor/github.com/jonboulle/clockwork/README.md b/vendor/github.com/jonboulle/clockwork/README.md new file mode 100644 index 00000000..42970da8 --- /dev/null +++ b/vendor/github.com/jonboulle/clockwork/README.md @@ -0,0 +1,80 @@ +# clockwork + +[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go#utilities) + +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/jonboulle/clockwork/ci.yaml?style=flat-square)](https://github.com/jonboulle/clockwork/actions?query=workflow%3ACI) +[![Go Report Card](https://goreportcard.com/badge/github.com/jonboulle/clockwork?style=flat-square)](https://goreportcard.com/report/github.com/jonboulle/clockwork) +![Go Version](https://img.shields.io/badge/go%20version-%3E=1.15-61CFDD.svg?style=flat-square) +[![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/jonboulle/clockwork) + +**A simple fake clock for Go.** + + +## Usage + +Replace uses of the `time` package with the `clockwork.Clock` interface instead. + +For example, instead of using `time.Sleep` directly: + +```go +func myFunc() { + time.Sleep(3 * time.Second) + doSomething() +} +``` + +Inject a clock and use its `Sleep` method instead: + +```go +func myFunc(clock clockwork.Clock) { + clock.Sleep(3 * time.Second) + doSomething() +} +``` + +Now you can easily test `myFunc` with a `FakeClock`: + +```go +func TestMyFunc(t *testing.T) { + c := clockwork.NewFakeClock() + + // Start our sleepy function + var wg sync.WaitGroup + wg.Add(1) + go func() { + myFunc(c) + wg.Done() + }() + + // Ensure we wait until myFunc is sleeping + c.BlockUntil(1) + + assertState() + + // Advance the FakeClock forward in time + c.Advance(3 * time.Second) + + // Wait until the function completes + wg.Wait() + + assertState() +} +``` + +and in production builds, simply inject the real clock instead: + +```go +myFunc(clockwork.NewRealClock()) +``` + +See [example_test.go](example_test.go) for a full example. + + +# Credits + +clockwork is inspired by @wickman's [threaded fake clock](https://gist.github.com/wickman/3840816), and the [Golang playground](https://blog.golang.org/playground#TOC_3.1.) + + +## License + +Apache License, Version 2.0. Please see [License File](LICENSE) for more information. diff --git a/vendor/github.com/jonboulle/clockwork/clockwork.go b/vendor/github.com/jonboulle/clockwork/clockwork.go new file mode 100644 index 00000000..3206b36e --- /dev/null +++ b/vendor/github.com/jonboulle/clockwork/clockwork.go @@ -0,0 +1,349 @@ +package clockwork + +import ( + "context" + "sort" + "sync" + "time" +) + +// Clock provides an interface that packages can use instead of directly using +// the [time] module, so that chronology-related behavior can be tested. +type Clock interface { + After(d time.Duration) <-chan time.Time + Sleep(d time.Duration) + Now() time.Time + Since(t time.Time) time.Duration + NewTicker(d time.Duration) Ticker + NewTimer(d time.Duration) Timer + AfterFunc(d time.Duration, f func()) Timer +} + +// FakeClock provides an interface for a clock which can be manually advanced +// through time. +// +// FakeClock maintains a list of "waiters," which consists of all callers +// waiting on the underlying clock (i.e. Tickers and Timers including callers of +// Sleep or After). Users can call BlockUntil to block until the clock has an +// expected number of waiters. +type FakeClock interface { + Clock + // Advance advances the FakeClock to a new point in time, ensuring any existing + // waiters are notified appropriately before returning. + Advance(d time.Duration) + // BlockUntil blocks until the FakeClock has the given number of waiters. + BlockUntil(waiters int) +} + +// NewRealClock returns a Clock which simply delegates calls to the actual time +// package; it should be used by packages in production. +func NewRealClock() Clock { + return &realClock{} +} + +// NewFakeClock returns a FakeClock implementation which can be +// manually advanced through time for testing. The initial time of the +// FakeClock will be the current system time. +// +// Tests that require a deterministic time must use NewFakeClockAt. +func NewFakeClock() FakeClock { + return NewFakeClockAt(time.Now()) +} + +// NewFakeClockAt returns a FakeClock initialised at the given time.Time. +func NewFakeClockAt(t time.Time) FakeClock { + return &fakeClock{ + time: t, + } +} + +type realClock struct{} + +func (rc *realClock) After(d time.Duration) <-chan time.Time { + return time.After(d) +} + +func (rc *realClock) Sleep(d time.Duration) { + time.Sleep(d) +} + +func (rc *realClock) Now() time.Time { + return time.Now() +} + +func (rc *realClock) Since(t time.Time) time.Duration { + return rc.Now().Sub(t) +} + +func (rc *realClock) NewTicker(d time.Duration) Ticker { + return realTicker{time.NewTicker(d)} +} + +func (rc *realClock) NewTimer(d time.Duration) Timer { + return realTimer{time.NewTimer(d)} +} + +func (rc *realClock) AfterFunc(d time.Duration, f func()) Timer { + return realTimer{time.AfterFunc(d, f)} +} + +type fakeClock struct { + // l protects all attributes of the clock, including all attributes of all + // waiters and blockers. + l sync.RWMutex + waiters []expirer + blockers []*blocker + time time.Time +} + +// blocker is a caller of BlockUntil. +type blocker struct { + count int + + // ch is closed when the underlying clock has the specificed number of blockers. + ch chan struct{} +} + +// expirer is a timer or ticker that expires at some point in the future. +type expirer interface { + // expire the expirer at the given time, returning the desired duration until + // the next expiration, if any. + expire(now time.Time) (next *time.Duration) + + // Get and set the expiration time. + expiry() time.Time + setExpiry(time.Time) +} + +// After mimics [time.After]; it waits for the given duration to elapse on the +// fakeClock, then sends the current time on the returned channel. +func (fc *fakeClock) After(d time.Duration) <-chan time.Time { + return fc.NewTimer(d).Chan() +} + +// Sleep blocks until the given duration has passed on the fakeClock. +func (fc *fakeClock) Sleep(d time.Duration) { + <-fc.After(d) +} + +// Now returns the current time of the fakeClock +func (fc *fakeClock) Now() time.Time { + fc.l.RLock() + defer fc.l.RUnlock() + return fc.time +} + +// Since returns the duration that has passed since the given time on the +// fakeClock. +func (fc *fakeClock) Since(t time.Time) time.Duration { + return fc.Now().Sub(t) +} + +// NewTicker returns a Ticker that will expire only after calls to +// fakeClock.Advance() have moved the clock past the given duration. +func (fc *fakeClock) NewTicker(d time.Duration) Ticker { + var ft *fakeTicker + ft = &fakeTicker{ + firer: newFirer(), + d: d, + reset: func(d time.Duration) { fc.set(ft, d) }, + stop: func() { fc.stop(ft) }, + } + fc.set(ft, d) + return ft +} + +// NewTimer returns a Timer that will fire only after calls to +// fakeClock.Advance() have moved the clock past the given duration. +func (fc *fakeClock) NewTimer(d time.Duration) Timer { + return fc.newTimer(d, nil) +} + +// AfterFunc mimics [time.AfterFunc]; it returns a Timer that will invoke the +// given function only after calls to fakeClock.Advance() have moved the clock +// past the given duration. +func (fc *fakeClock) AfterFunc(d time.Duration, f func()) Timer { + return fc.newTimer(d, f) +} + +// newTimer returns a new timer, using an optional afterFunc. +func (fc *fakeClock) newTimer(d time.Duration, afterfunc func()) *fakeTimer { + var ft *fakeTimer + ft = &fakeTimer{ + firer: newFirer(), + reset: func(d time.Duration) bool { + fc.l.Lock() + defer fc.l.Unlock() + // fc.l must be held across the calls to stopExpirer & setExpirer. + stopped := fc.stopExpirer(ft) + fc.setExpirer(ft, d) + return stopped + }, + stop: func() bool { return fc.stop(ft) }, + + afterFunc: afterfunc, + } + fc.set(ft, d) + return ft +} + +// Advance advances fakeClock to a new point in time, ensuring waiters and +// blockers are notified appropriately before returning. +func (fc *fakeClock) Advance(d time.Duration) { + fc.l.Lock() + defer fc.l.Unlock() + end := fc.time.Add(d) + // Expire the earliest waiter until the earliest waiter's expiration is after + // end. + // + // We don't iterate because the callback of the waiter might register a new + // waiter, so the list of waiters might change as we execute this. + for len(fc.waiters) > 0 && !end.Before(fc.waiters[0].expiry()) { + w := fc.waiters[0] + fc.waiters = fc.waiters[1:] + + // Use the waiter's expriation as the current time for this expiration. + now := w.expiry() + fc.time = now + if d := w.expire(now); d != nil { + // Set the new exipration if needed. + fc.setExpirer(w, *d) + } + } + fc.time = end +} + +// BlockUntil blocks until the fakeClock has the given number of waiters. +// +// Prefer BlockUntilContext, which offers context cancellation to prevent +// deadlock. +// +// Deprecation warning: This function might be deprecated in later versions. +func (fc *fakeClock) BlockUntil(n int) { + b := fc.newBlocker(n) + if b == nil { + return + } + <-b.ch +} + +// BlockUntilContext blocks until the fakeClock has the given number of waiters +// or the context is cancelled. +func (fc *fakeClock) BlockUntilContext(ctx context.Context, n int) error { + b := fc.newBlocker(n) + if b == nil { + return nil + } + + select { + case <-b.ch: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +func (fc *fakeClock) newBlocker(n int) *blocker { + fc.l.Lock() + defer fc.l.Unlock() + // Fast path: we already have >= n waiters. + if len(fc.waiters) >= n { + return nil + } + // Set up a new blocker to wait for more waiters. + b := &blocker{ + count: n, + ch: make(chan struct{}), + } + fc.blockers = append(fc.blockers, b) + return b +} + +// stop stops an expirer, returning true if the expirer was stopped. +func (fc *fakeClock) stop(e expirer) bool { + fc.l.Lock() + defer fc.l.Unlock() + return fc.stopExpirer(e) +} + +// stopExpirer stops an expirer, returning true if the expirer was stopped. +// +// The caller must hold fc.l. +func (fc *fakeClock) stopExpirer(e expirer) bool { + for i, t := range fc.waiters { + if t == e { + // Remove element, maintaining order. + copy(fc.waiters[i:], fc.waiters[i+1:]) + fc.waiters[len(fc.waiters)-1] = nil + fc.waiters = fc.waiters[:len(fc.waiters)-1] + return true + } + } + return false +} + +// set sets an expirer to expire at a future point in time. +func (fc *fakeClock) set(e expirer, d time.Duration) { + fc.l.Lock() + defer fc.l.Unlock() + fc.setExpirer(e, d) +} + +// setExpirer sets an expirer to expire at a future point in time. +// +// The caller must hold fc.l. +func (fc *fakeClock) setExpirer(e expirer, d time.Duration) { + if d.Nanoseconds() <= 0 { + // special case - trigger immediately, never reset. + // + // TODO: Explain what cases this covers. + e.expire(fc.time) + return + } + // Add the expirer to the set of waiters and notify any blockers. + e.setExpiry(fc.time.Add(d)) + fc.waiters = append(fc.waiters, e) + sort.Slice(fc.waiters, func(i int, j int) bool { + return fc.waiters[i].expiry().Before(fc.waiters[j].expiry()) + }) + + // Notify blockers of our new waiter. + var blocked []*blocker + count := len(fc.waiters) + for _, b := range fc.blockers { + if b.count <= count { + close(b.ch) + continue + } + blocked = append(blocked, b) + } + fc.blockers = blocked +} + +// firer is used by fakeTimer and fakeTicker used to help implement expirer. +type firer struct { + // The channel associated with the firer, used to send expriation times. + c chan time.Time + + // The time when the firer expires. Only meaningful if the firer is currently + // one of a fakeClock's waiters. + exp time.Time +} + +func newFirer() firer { + return firer{c: make(chan time.Time, 1)} +} + +func (f *firer) Chan() <-chan time.Time { + return f.c +} + +// expiry implements expirer. +func (f *firer) expiry() time.Time { + return f.exp +} + +// setExpiry implements expirer. +func (f *firer) setExpiry(t time.Time) { + f.exp = t +} diff --git a/vendor/github.com/jonboulle/clockwork/context.go b/vendor/github.com/jonboulle/clockwork/context.go new file mode 100644 index 00000000..edbb368f --- /dev/null +++ b/vendor/github.com/jonboulle/clockwork/context.go @@ -0,0 +1,25 @@ +package clockwork + +import ( + "context" +) + +// contextKey is private to this package so we can ensure uniqueness here. This +// type identifies context values provided by this package. +type contextKey string + +// keyClock provides a clock for injecting during tests. If absent, a real clock should be used. +var keyClock = contextKey("clock") // clockwork.Clock + +// AddToContext creates a derived context that references the specified clock. +func AddToContext(ctx context.Context, clock Clock) context.Context { + return context.WithValue(ctx, keyClock, clock) +} + +// FromContext extracts a clock from the context. If not present, a real clock is returned. +func FromContext(ctx context.Context) Clock { + if clock, ok := ctx.Value(keyClock).(Clock); ok { + return clock + } + return NewRealClock() +} diff --git a/vendor/github.com/jonboulle/clockwork/ticker.go b/vendor/github.com/jonboulle/clockwork/ticker.go new file mode 100644 index 00000000..b68e4d77 --- /dev/null +++ b/vendor/github.com/jonboulle/clockwork/ticker.go @@ -0,0 +1,48 @@ +package clockwork + +import "time" + +// Ticker provides an interface which can be used instead of directly using +// [time.Ticker]. The real-time ticker t provides ticks through t.C which +// becomes t.Chan() to make this channel requirement definable in this +// interface. +type Ticker interface { + Chan() <-chan time.Time + Reset(d time.Duration) + Stop() +} + +type realTicker struct{ *time.Ticker } + +func (r realTicker) Chan() <-chan time.Time { + return r.C +} + +type fakeTicker struct { + firer + + // reset and stop provide the implementation of the respective exported + // functions. + reset func(d time.Duration) + stop func() + + // The duration of the ticker. + d time.Duration +} + +func (f *fakeTicker) Reset(d time.Duration) { + f.reset(d) +} + +func (f *fakeTicker) Stop() { + f.stop() +} + +func (f *fakeTicker) expire(now time.Time) *time.Duration { + // Never block on expiration. + select { + case f.c <- now: + default: + } + return &f.d +} diff --git a/vendor/github.com/jonboulle/clockwork/timer.go b/vendor/github.com/jonboulle/clockwork/timer.go new file mode 100644 index 00000000..6f928b3d --- /dev/null +++ b/vendor/github.com/jonboulle/clockwork/timer.go @@ -0,0 +1,53 @@ +package clockwork + +import "time" + +// Timer provides an interface which can be used instead of directly using +// [time.Timer]. The real-time timer t provides events through t.C which becomes +// t.Chan() to make this channel requirement definable in this interface. +type Timer interface { + Chan() <-chan time.Time + Reset(d time.Duration) bool + Stop() bool +} + +type realTimer struct{ *time.Timer } + +func (r realTimer) Chan() <-chan time.Time { + return r.C +} + +type fakeTimer struct { + firer + + // reset and stop provide the implmenetation of the respective exported + // functions. + reset func(d time.Duration) bool + stop func() bool + + // If present when the timer fires, the timer calls afterFunc in its own + // goroutine rather than sending the time on Chan(). + afterFunc func() +} + +func (f *fakeTimer) Reset(d time.Duration) bool { + return f.reset(d) +} + +func (f *fakeTimer) Stop() bool { + return f.stop() +} + +func (f *fakeTimer) expire(now time.Time) *time.Duration { + if f.afterFunc != nil { + go f.afterFunc() + return nil + } + + // Never block on expiration. + select { + case f.c <- now: + default: + } + return nil +} diff --git a/vendor/golang.org/x/time/LICENSE b/vendor/golang.org/x/time/LICENSE new file mode 100644 index 00000000..6a66aea5 --- /dev/null +++ b/vendor/golang.org/x/time/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/time/PATENTS b/vendor/golang.org/x/time/PATENTS new file mode 100644 index 00000000..73309904 --- /dev/null +++ b/vendor/golang.org/x/time/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/time/rate/rate.go b/vendor/golang.org/x/time/rate/rate.go new file mode 100644 index 00000000..f0e0cf3c --- /dev/null +++ b/vendor/golang.org/x/time/rate/rate.go @@ -0,0 +1,428 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package rate provides a rate limiter. +package rate + +import ( + "context" + "fmt" + "math" + "sync" + "time" +) + +// Limit defines the maximum frequency of some events. +// Limit is represented as number of events per second. +// A zero Limit allows no events. +type Limit float64 + +// Inf is the infinite rate limit; it allows all events (even if burst is zero). +const Inf = Limit(math.MaxFloat64) + +// Every converts a minimum time interval between events to a Limit. +func Every(interval time.Duration) Limit { + if interval <= 0 { + return Inf + } + return 1 / Limit(interval.Seconds()) +} + +// A Limiter controls how frequently events are allowed to happen. +// It implements a "token bucket" of size b, initially full and refilled +// at rate r tokens per second. +// Informally, in any large enough time interval, the Limiter limits the +// rate to r tokens per second, with a maximum burst size of b events. +// As a special case, if r == Inf (the infinite rate), b is ignored. +// See https://en.wikipedia.org/wiki/Token_bucket for more about token buckets. +// +// The zero value is a valid Limiter, but it will reject all events. +// Use NewLimiter to create non-zero Limiters. +// +// Limiter has three main methods, Allow, Reserve, and Wait. +// Most callers should use Wait. +// +// Each of the three methods consumes a single token. +// They differ in their behavior when no token is available. +// If no token is available, Allow returns false. +// If no token is available, Reserve returns a reservation for a future token +// and the amount of time the caller must wait before using it. +// If no token is available, Wait blocks until one can be obtained +// or its associated context.Context is canceled. +// +// The methods AllowN, ReserveN, and WaitN consume n tokens. +type Limiter struct { + mu sync.Mutex + limit Limit + burst int + tokens float64 + // last is the last time the limiter's tokens field was updated + last time.Time + // lastEvent is the latest time of a rate-limited event (past or future) + lastEvent time.Time +} + +// Limit returns the maximum overall event rate. +func (lim *Limiter) Limit() Limit { + lim.mu.Lock() + defer lim.mu.Unlock() + return lim.limit +} + +// Burst returns the maximum burst size. Burst is the maximum number of tokens +// that can be consumed in a single call to Allow, Reserve, or Wait, so higher +// Burst values allow more events to happen at once. +// A zero Burst allows no events, unless limit == Inf. +func (lim *Limiter) Burst() int { + lim.mu.Lock() + defer lim.mu.Unlock() + return lim.burst +} + +// TokensAt returns the number of tokens available at time t. +func (lim *Limiter) TokensAt(t time.Time) float64 { + lim.mu.Lock() + _, tokens := lim.advance(t) // does not mutate lim + lim.mu.Unlock() + return tokens +} + +// Tokens returns the number of tokens available now. +func (lim *Limiter) Tokens() float64 { + return lim.TokensAt(time.Now()) +} + +// NewLimiter returns a new Limiter that allows events up to rate r and permits +// bursts of at most b tokens. +func NewLimiter(r Limit, b int) *Limiter { + return &Limiter{ + limit: r, + burst: b, + } +} + +// Allow reports whether an event may happen now. +func (lim *Limiter) Allow() bool { + return lim.AllowN(time.Now(), 1) +} + +// AllowN reports whether n events may happen at time t. +// Use this method if you intend to drop / skip events that exceed the rate limit. +// Otherwise use Reserve or Wait. +func (lim *Limiter) AllowN(t time.Time, n int) bool { + return lim.reserveN(t, n, 0).ok +} + +// A Reservation holds information about events that are permitted by a Limiter to happen after a delay. +// A Reservation may be canceled, which may enable the Limiter to permit additional events. +type Reservation struct { + ok bool + lim *Limiter + tokens int + timeToAct time.Time + // This is the Limit at reservation time, it can change later. + limit Limit +} + +// OK returns whether the limiter can provide the requested number of tokens +// within the maximum wait time. If OK is false, Delay returns InfDuration, and +// Cancel does nothing. +func (r *Reservation) OK() bool { + return r.ok +} + +// Delay is shorthand for DelayFrom(time.Now()). +func (r *Reservation) Delay() time.Duration { + return r.DelayFrom(time.Now()) +} + +// InfDuration is the duration returned by Delay when a Reservation is not OK. +const InfDuration = time.Duration(math.MaxInt64) + +// DelayFrom returns the duration for which the reservation holder must wait +// before taking the reserved action. Zero duration means act immediately. +// InfDuration means the limiter cannot grant the tokens requested in this +// Reservation within the maximum wait time. +func (r *Reservation) DelayFrom(t time.Time) time.Duration { + if !r.ok { + return InfDuration + } + delay := r.timeToAct.Sub(t) + if delay < 0 { + return 0 + } + return delay +} + +// Cancel is shorthand for CancelAt(time.Now()). +func (r *Reservation) Cancel() { + r.CancelAt(time.Now()) +} + +// CancelAt indicates that the reservation holder will not perform the reserved action +// and reverses the effects of this Reservation on the rate limit as much as possible, +// considering that other reservations may have already been made. +func (r *Reservation) CancelAt(t time.Time) { + if !r.ok { + return + } + + r.lim.mu.Lock() + defer r.lim.mu.Unlock() + + if r.lim.limit == Inf || r.tokens == 0 || r.timeToAct.Before(t) { + return + } + + // calculate tokens to restore + // The duration between lim.lastEvent and r.timeToAct tells us how many tokens were reserved + // after r was obtained. These tokens should not be restored. + restoreTokens := float64(r.tokens) - r.limit.tokensFromDuration(r.lim.lastEvent.Sub(r.timeToAct)) + if restoreTokens <= 0 { + return + } + // advance time to now + t, tokens := r.lim.advance(t) + // calculate new number of tokens + tokens += restoreTokens + if burst := float64(r.lim.burst); tokens > burst { + tokens = burst + } + // update state + r.lim.last = t + r.lim.tokens = tokens + if r.timeToAct == r.lim.lastEvent { + prevEvent := r.timeToAct.Add(r.limit.durationFromTokens(float64(-r.tokens))) + if !prevEvent.Before(t) { + r.lim.lastEvent = prevEvent + } + } +} + +// Reserve is shorthand for ReserveN(time.Now(), 1). +func (lim *Limiter) Reserve() *Reservation { + return lim.ReserveN(time.Now(), 1) +} + +// ReserveN returns a Reservation that indicates how long the caller must wait before n events happen. +// The Limiter takes this Reservation into account when allowing future events. +// The returned Reservation’s OK() method returns false if n exceeds the Limiter's burst size. +// Usage example: +// +// r := lim.ReserveN(time.Now(), 1) +// if !r.OK() { +// // Not allowed to act! Did you remember to set lim.burst to be > 0 ? +// return +// } +// time.Sleep(r.Delay()) +// Act() +// +// Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events. +// If you need to respect a deadline or cancel the delay, use Wait instead. +// To drop or skip events exceeding rate limit, use Allow instead. +func (lim *Limiter) ReserveN(t time.Time, n int) *Reservation { + r := lim.reserveN(t, n, InfDuration) + return &r +} + +// Wait is shorthand for WaitN(ctx, 1). +func (lim *Limiter) Wait(ctx context.Context) (err error) { + return lim.WaitN(ctx, 1) +} + +// WaitN blocks until lim permits n events to happen. +// It returns an error if n exceeds the Limiter's burst size, the Context is +// canceled, or the expected wait time exceeds the Context's Deadline. +// The burst limit is ignored if the rate limit is Inf. +func (lim *Limiter) WaitN(ctx context.Context, n int) (err error) { + // The test code calls lim.wait with a fake timer generator. + // This is the real timer generator. + newTimer := func(d time.Duration) (<-chan time.Time, func() bool, func()) { + timer := time.NewTimer(d) + return timer.C, timer.Stop, func() {} + } + + return lim.wait(ctx, n, time.Now(), newTimer) +} + +// wait is the internal implementation of WaitN. +func (lim *Limiter) wait(ctx context.Context, n int, t time.Time, newTimer func(d time.Duration) (<-chan time.Time, func() bool, func())) error { + lim.mu.Lock() + burst := lim.burst + limit := lim.limit + lim.mu.Unlock() + + if n > burst && limit != Inf { + return fmt.Errorf("rate: Wait(n=%d) exceeds limiter's burst %d", n, burst) + } + // Check if ctx is already cancelled + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + // Determine wait limit + waitLimit := InfDuration + if deadline, ok := ctx.Deadline(); ok { + waitLimit = deadline.Sub(t) + } + // Reserve + r := lim.reserveN(t, n, waitLimit) + if !r.ok { + return fmt.Errorf("rate: Wait(n=%d) would exceed context deadline", n) + } + // Wait if necessary + delay := r.DelayFrom(t) + if delay == 0 { + return nil + } + ch, stop, advance := newTimer(delay) + defer stop() + advance() // only has an effect when testing + select { + case <-ch: + // We can proceed. + return nil + case <-ctx.Done(): + // Context was canceled before we could proceed. Cancel the + // reservation, which may permit other events to proceed sooner. + r.Cancel() + return ctx.Err() + } +} + +// SetLimit is shorthand for SetLimitAt(time.Now(), newLimit). +func (lim *Limiter) SetLimit(newLimit Limit) { + lim.SetLimitAt(time.Now(), newLimit) +} + +// SetLimitAt sets a new Limit for the limiter. The new Limit, and Burst, may be violated +// or underutilized by those which reserved (using Reserve or Wait) but did not yet act +// before SetLimitAt was called. +func (lim *Limiter) SetLimitAt(t time.Time, newLimit Limit) { + lim.mu.Lock() + defer lim.mu.Unlock() + + t, tokens := lim.advance(t) + + lim.last = t + lim.tokens = tokens + lim.limit = newLimit +} + +// SetBurst is shorthand for SetBurstAt(time.Now(), newBurst). +func (lim *Limiter) SetBurst(newBurst int) { + lim.SetBurstAt(time.Now(), newBurst) +} + +// SetBurstAt sets a new burst size for the limiter. +func (lim *Limiter) SetBurstAt(t time.Time, newBurst int) { + lim.mu.Lock() + defer lim.mu.Unlock() + + t, tokens := lim.advance(t) + + lim.last = t + lim.tokens = tokens + lim.burst = newBurst +} + +// reserveN is a helper method for AllowN, ReserveN, and WaitN. +// maxFutureReserve specifies the maximum reservation wait duration allowed. +// reserveN returns Reservation, not *Reservation, to avoid allocation in AllowN and WaitN. +func (lim *Limiter) reserveN(t time.Time, n int, maxFutureReserve time.Duration) Reservation { + lim.mu.Lock() + defer lim.mu.Unlock() + + if lim.limit == Inf { + return Reservation{ + ok: true, + lim: lim, + tokens: n, + timeToAct: t, + } + } else if lim.limit == 0 { + var ok bool + if lim.burst >= n { + ok = true + lim.burst -= n + } + return Reservation{ + ok: ok, + lim: lim, + tokens: lim.burst, + timeToAct: t, + } + } + + t, tokens := lim.advance(t) + + // Calculate the remaining number of tokens resulting from the request. + tokens -= float64(n) + + // Calculate the wait duration + var waitDuration time.Duration + if tokens < 0 { + waitDuration = lim.limit.durationFromTokens(-tokens) + } + + // Decide result + ok := n <= lim.burst && waitDuration <= maxFutureReserve + + // Prepare reservation + r := Reservation{ + ok: ok, + lim: lim, + limit: lim.limit, + } + if ok { + r.tokens = n + r.timeToAct = t.Add(waitDuration) + + // Update state + lim.last = t + lim.tokens = tokens + lim.lastEvent = r.timeToAct + } + + return r +} + +// advance calculates and returns an updated state for lim resulting from the passage of time. +// lim is not changed. +// advance requires that lim.mu is held. +func (lim *Limiter) advance(t time.Time) (newT time.Time, newTokens float64) { + last := lim.last + if t.Before(last) { + last = t + } + + // Calculate the new number of tokens, due to time that passed. + elapsed := t.Sub(last) + delta := lim.limit.tokensFromDuration(elapsed) + tokens := lim.tokens + delta + if burst := float64(lim.burst); tokens > burst { + tokens = burst + } + return t, tokens +} + +// durationFromTokens is a unit conversion function from the number of tokens to the duration +// of time it takes to accumulate them at a rate of limit tokens per second. +func (limit Limit) durationFromTokens(tokens float64) time.Duration { + if limit <= 0 { + return InfDuration + } + seconds := tokens / float64(limit) + return time.Duration(float64(time.Second) * seconds) +} + +// tokensFromDuration is a unit conversion function from a time duration to the number of tokens +// which could be accumulated during that duration at a rate of limit tokens per second. +func (limit Limit) tokensFromDuration(d time.Duration) float64 { + if limit <= 0 { + return 0 + } + return d.Seconds() * float64(limit) +} diff --git a/vendor/golang.org/x/time/rate/sometimes.go b/vendor/golang.org/x/time/rate/sometimes.go new file mode 100644 index 00000000..6ba99ddb --- /dev/null +++ b/vendor/golang.org/x/time/rate/sometimes.go @@ -0,0 +1,67 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package rate + +import ( + "sync" + "time" +) + +// Sometimes will perform an action occasionally. The First, Every, and +// Interval fields govern the behavior of Do, which performs the action. +// A zero Sometimes value will perform an action exactly once. +// +// # Example: logging with rate limiting +// +// var sometimes = rate.Sometimes{First: 3, Interval: 10*time.Second} +// func Spammy() { +// sometimes.Do(func() { log.Info("here I am!") }) +// } +type Sometimes struct { + First int // if non-zero, the first N calls to Do will run f. + Every int // if non-zero, every Nth call to Do will run f. + Interval time.Duration // if non-zero and Interval has elapsed since f's last run, Do will run f. + + mu sync.Mutex + count int // number of Do calls + last time.Time // last time f was run +} + +// Do runs the function f as allowed by First, Every, and Interval. +// +// The model is a union (not intersection) of filters. The first call to Do +// always runs f. Subsequent calls to Do run f if allowed by First or Every or +// Interval. +// +// A non-zero First:N causes the first N Do(f) calls to run f. +// +// A non-zero Every:M causes every Mth Do(f) call, starting with the first, to +// run f. +// +// A non-zero Interval causes Do(f) to run f if Interval has elapsed since +// Do last ran f. +// +// Specifying multiple filters produces the union of these execution streams. +// For example, specifying both First:N and Every:M causes the first N Do(f) +// calls and every Mth Do(f) call, starting with the first, to run f. See +// Examples for more. +// +// If Do is called multiple times simultaneously, the calls will block and run +// serially. Therefore, Do is intended for lightweight operations. +// +// Because a call to Do may block until f returns, if f causes Do to be called, +// it will deadlock. +func (s *Sometimes) Do(f func()) { + s.mu.Lock() + defer s.mu.Unlock() + if s.count == 0 || + (s.First > 0 && s.count < s.First) || + (s.Every > 0 && s.count%s.Every == 0) || + (s.Interval > 0 && time.Since(s.last) >= s.Interval) { + f() + s.last = time.Now() + } + s.count++ +} From 6f09f253104f62480f2339998b4f6b4a0db61d1c Mon Sep 17 00:00:00 2001 From: Denis Krivosheev Date: Mon, 3 Apr 2023 11:40:39 +0300 Subject: [PATCH 11/13] Additional transport test Add test for a case when rate limiter does not allow a request. --- internal/proxy/transport_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/internal/proxy/transport_test.go b/internal/proxy/transport_test.go index d6036e07..0436c4be 100644 --- a/internal/proxy/transport_test.go +++ b/internal/proxy/transport_test.go @@ -3,6 +3,7 @@ package proxy import ( "net/http" "testing" + "time" "github.com/rekby/lets-proxy2/internal/th" @@ -37,3 +38,25 @@ func TestTransport_GetTransport(t *testing.T) { td.Cmp(httpTransport.TLSClientConfig.ServerName, "www.ru") td.Cmp(httpTransport.TLSClientConfig.InsecureSkipVerify, true) } + +func TestTransport_RoundTrip(t *testing.T) { + t.Run("should return status 429 when request is not allowed by rate limiter", func(t *testing.T) { + td := testdeep.NewT(t) + rateLimiter, err := NewRateLimiter(RateLimitParams{ + // -1 force rate limiter to never allow a request + RateLimit: -1, + TimeWindow: time.Second, + Burst: 1, + CacheSize: 100, + }) + td.CmpNoError(err) + + tr := Transport{RateLimiter: rateLimiter} + req, _ := http.NewRequest(http.MethodGet, "http://www.ru", nil) + + resp, err := tr.RoundTrip(req) + + td.CmpNoError(err) + td.Cmp(resp.StatusCode, http.StatusTooManyRequests, "should return '429 Too Many Request'") + }) +} From da9d33cada72af4996538790bc2a2509d967f46b Mon Sep 17 00:00:00 2001 From: Denis Krivosheev Date: Mon, 3 Apr 2023 11:55:02 +0300 Subject: [PATCH 12/13] Add tests for rate limiter edge cases --- internal/proxy/rate_limiter_test.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/internal/proxy/rate_limiter_test.go b/internal/proxy/rate_limiter_test.go index bf74ff11..4d5db08b 100644 --- a/internal/proxy/rate_limiter_test.go +++ b/internal/proxy/rate_limiter_test.go @@ -10,6 +10,22 @@ import ( "github.com/maxatome/go-testdeep" ) +func TestRateLimiter_Allow(t *testing.T) { + t.Run("should trigger an error if cache size is less than zero", func(t *testing.T) { + _, err := NewRateLimiter(RateLimitParams{RateLimit: 1, CacheSize: -1}) + testdeep.CmpError(t, err) + }) + + t.Run("should always allow if rate limit is zero", func(t *testing.T) { + td := testdeep.NewT(t) + rateLimiter, err := NewRateLimiter(RateLimitParams{RateLimit: 0}) + req, _ := http.NewRequest(http.MethodGet, "http://url.com", nil) + + td.CmpNoError(err) + td.True(rateLimiter.Allow(req)) + }) +} + func TestMaxRequestsPerSec(t *testing.T) { req1, _ := http.NewRequest("GET", "http://url1.com", nil) req1.RemoteAddr = "ip1" @@ -142,5 +158,4 @@ func TestMaxRequestsPerSec(t *testing.T) { } }) } - } From b8213c14aa534d4d217472a95b5481ef81e741f1 Mon Sep 17 00:00:00 2001 From: Denis Krivosheev Date: Mon, 3 Apr 2023 12:28:34 +0300 Subject: [PATCH 13/13] Rename RateLimitTimeWindow to RateLimitTimeWindowMs --- cmd/static/default-config.toml | 2 +- internal/proxy/config.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/static/default-config.toml b/cmd/static/default-config.toml index 7ef793ed..b20cfeee 100644 --- a/cmd/static/default-config.toml +++ b/cmd/static/default-config.toml @@ -112,7 +112,7 @@ HTTPSBackendIgnoreCert = true RateLimit = 0 # Value in milliseconds -RateLimitTimeWindow = 1000 +RateLimitTimeWindowMs = 1000 # The number of requests per host that can be handled in paralles. The default value 0 # means the value will be the same as RateLimit diff --git a/internal/proxy/config.go b/internal/proxy/config.go index d01f533a..1044cf5b 100644 --- a/internal/proxy/config.go +++ b/internal/proxy/config.go @@ -27,7 +27,7 @@ type Config struct { HTTPSBackendIgnoreCert bool EnableAccessLog bool RateLimit int - RateLimitTimeWindow int + RateLimitTimeWindowMs int RateLimitBurst int RateLimitCacheSize int } @@ -48,7 +48,7 @@ func (c *Config) Apply(ctx context.Context, p *HTTPProxy) error { rateLimiter, resErr := NewRateLimiter(RateLimitParams{ RateLimit: c.RateLimit, - TimeWindow: time.Duration(c.RateLimitTimeWindow) * time.Millisecond, + TimeWindow: time.Duration(c.RateLimitTimeWindowMs) * time.Millisecond, Burst: c.RateLimitBurst, CacheSize: c.RateLimitCacheSize, })