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  }