github.com/livekit/protocol@v1.16.1-0.20240517185851-47e4c6bba773/utils/rate.go (about) 1 // Copyright (c) 2016,2020 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 // 21 // SOURCE: https://github.com/uber-go/ratelimit/blob/main/limiter_mutexbased.go 22 // EDIT: slight modification to allow setting rate limit on the fly 23 // SCOPE: LeakyBucket 24 package utils 25 26 import ( 27 "sync" 28 "time" 29 30 "go.uber.org/atomic" 31 ) 32 33 type LeakyBucket struct { 34 mutex sync.Mutex 35 last time.Time 36 sleepFor time.Duration 37 cfg atomic.Pointer[leakyBucketConfig] 38 clock Clock 39 } 40 41 type leakyBucketConfig struct { 42 perRequest time.Duration 43 maxSlack time.Duration 44 } 45 46 // NewLeakyBucket initiates LeakyBucket with rateLimit, slack, and clock. 47 // 48 // rateLimit is defined as the number of request per second. 49 // 50 // slack is defined as the number of allowed requests before limiting. 51 // e.g. when slack=5, LeakyBucket will allow 5 requests to pass through Take 52 // without a sleep as long as these requests are under perRequest duration. 53 func NewLeakyBucket(rateLimit int, slack int, clock Clock) *LeakyBucket { 54 var lb LeakyBucket 55 lb.clock = clock 56 lb.Update(rateLimit, slack) 57 return &lb 58 } 59 60 // Update sets the underlying rate limit and slack. 61 // The setting may not be applied immediately. 62 // 63 // Update is THREAD SAFE and NON-BLOCKING. 64 func (lb *LeakyBucket) Update(rateLimit int, slack int) { 65 perRequest := time.Second / time.Duration(rateLimit) 66 maxSlack := -1 * time.Duration(slack) * perRequest 67 cfg := leakyBucketConfig{ 68 perRequest: perRequest, 69 maxSlack: maxSlack, 70 } 71 lb.cfg.Store(&cfg) 72 } 73 74 // Take blocks to ensure that the time spent between multiple Take calls 75 // is on average time.Second/rate. 76 // 77 // Take is THREAD SAFE and BLOCKING. 78 func (lb *LeakyBucket) Take() time.Time { 79 lb.mutex.Lock() 80 defer lb.mutex.Unlock() 81 82 cfg := lb.cfg.Load() 83 now := lb.clock.Now() 84 85 // If this is our first request, then we allow it. 86 if lb.last.IsZero() { 87 lb.last = now 88 return lb.last 89 } 90 91 // sleepFor calculates how much time we should sleep based on 92 // the perRequest budget and how long the last request took. 93 // Since the request may take longer than the budget, this number 94 // can get negative, and is summed across requests. 95 lb.sleepFor += cfg.perRequest - now.Sub(lb.last) 96 97 // We shouldn't allow sleepFor to get too negative, since it would mean that 98 // a service that slowed down a lot for a short period of time would get 99 // a much higher RPS following that. 100 if lb.sleepFor < cfg.maxSlack { 101 lb.sleepFor = cfg.maxSlack 102 } 103 104 // If sleepFor is positive, then we should sleep now. 105 if lb.sleepFor > 0 { 106 lb.clock.Sleep(lb.sleepFor) 107 lb.last = now.Add(lb.sleepFor) 108 lb.sleepFor = 0 109 } else { 110 lb.last = now 111 } 112 113 return lb.last 114 }