├── .gitignore ├── .github └── FUNDING.yml ├── .travis.yml ├── LICENSE ├── rate_test.go ├── rate.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: beefsack 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.7" 5 | - "1.8" 6 | - "1.9" 7 | - "1.10" 8 | - tip 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Michael Alexander 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rate_test.go: -------------------------------------------------------------------------------- 1 | package rate 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestRateLimiter_Wait_noblock(t *testing.T) { 9 | start := time.Now() 10 | limit := 5 11 | interval := time.Second * 3 12 | limiter := New(limit, interval) 13 | for i := 0; i < limit; i++ { 14 | limiter.Wait() 15 | } 16 | if time.Now().Sub(start) >= interval { 17 | t.Error("The limiter blocked when it shouldn't have") 18 | } 19 | } 20 | 21 | func TestRateLimiter_Wait_block(t *testing.T) { 22 | start := time.Now() 23 | limit := 5 24 | interval := time.Second * 3 25 | limiter := New(limit, interval) 26 | for i := 0; i < limit+1; i++ { 27 | limiter.Wait() 28 | } 29 | if time.Now().Sub(start) < interval { 30 | t.Error("The limiter didn't block when it should have") 31 | } 32 | } 33 | 34 | func TestRateLimiter_Try(t *testing.T) { 35 | limit := 5 36 | interval := time.Second * 3 37 | limiter := New(limit, interval) 38 | for i := 0; i < limit; i++ { 39 | if ok, _ := limiter.Try(); !ok { 40 | t.Fatalf("Should have allowed try on attempt %d", i) 41 | } 42 | } 43 | if ok, _ := limiter.Try(); ok { 44 | t.Fatal("Should have not allowed try on final attempt") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /rate.go: -------------------------------------------------------------------------------- 1 | package rate 2 | 3 | import ( 4 | "container/list" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // A RateLimiter limits the rate at which an action can be performed. It 10 | // applies neither smoothing (like one could achieve in a token bucket system) 11 | // nor does it offer any conception of warmup, wherein the rate of actions 12 | // granted are steadily increased until a steady throughput equilibrium is 13 | // reached. 14 | type RateLimiter struct { 15 | limit int 16 | interval time.Duration 17 | mtx sync.Mutex 18 | times list.List 19 | } 20 | 21 | // New creates a new rate limiter for the limit and interval. 22 | func New(limit int, interval time.Duration) *RateLimiter { 23 | lim := &RateLimiter{ 24 | limit: limit, 25 | interval: interval, 26 | } 27 | lim.times.Init() 28 | return lim 29 | } 30 | 31 | // Wait blocks if the rate limit has been reached. Wait offers no guarantees 32 | // of fairness for multiple actors if the allowed rate has been temporarily 33 | // exhausted. 34 | func (r *RateLimiter) Wait() { 35 | for { 36 | ok, remaining := r.Try() 37 | if ok { 38 | break 39 | } 40 | time.Sleep(remaining) 41 | } 42 | } 43 | 44 | // Try returns true if under the rate limit, or false if over and the 45 | // remaining time before the rate limit expires. 46 | func (r *RateLimiter) Try() (ok bool, remaining time.Duration) { 47 | r.mtx.Lock() 48 | defer r.mtx.Unlock() 49 | now := time.Now() 50 | if l := r.times.Len(); l < r.limit { 51 | r.times.PushBack(now) 52 | return true, 0 53 | } 54 | frnt := r.times.Front() 55 | if diff := now.Sub(frnt.Value.(time.Time)); diff < r.interval { 56 | return false, r.interval - diff 57 | } 58 | frnt.Value = now 59 | r.times.MoveToBack(frnt) 60 | return true, 0 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-rate 2 | =============== 3 | 4 | [![Build Status](https://travis-ci.org/beefsack/go-rate.svg?branch=master)](https://travis-ci.org/beefsack/go-rate) 5 | [![GoDoc](https://godoc.org/github.com/beefsack/go-rate?status.svg)](https://godoc.org/github.com/beefsack/go-rate) 6 | 7 | **go-rate** is a rate limiter designed for a range of use cases, 8 | including server side spam protection and preventing saturation of APIs you 9 | consume. 10 | 11 | It is used in production at 12 | [LangTrend](http://langtrend.com/l/Java,PHP,JavaScript) to adhere to the GitHub 13 | API rate limits. 14 | 15 | Usage 16 | ----- 17 | 18 | Import `github.com/beefsack/go-rate` and create a new rate limiter with 19 | the `rate.New(limit int, interval time.Duration)` function. 20 | 21 | The rate limiter provides a `Wait()` and a `Try() (bool, time.Duration)` method 22 | for both blocking and non-blocking functionality respectively. 23 | 24 | API documentation available at [godoc.org](http://godoc.org/github.com/beefsack/go-rate). 25 | 26 | Examples 27 | -------- 28 | 29 | ### Blocking rate limiting 30 | 31 | This example demonstrates limiting the output rate to 3 times per second. 32 | 33 | ```Go 34 | package main 35 | 36 | import ( 37 | "fmt" 38 | "time" 39 | 40 | "github.com/beefsack/go-rate" 41 | ) 42 | 43 | func main() { 44 | rl := rate.New(3, time.Second) // 3 times per second 45 | begin := time.Now() 46 | for i := 1; i <= 10; i++ { 47 | rl.Wait() 48 | fmt.Printf("%d started at %s\n", i, time.Now().Sub(begin)) 49 | } 50 | // Output: 51 | // 1 started at 12.584us 52 | // 2 started at 40.13us 53 | // 3 started at 44.92us 54 | // 4 started at 1.000125362s 55 | // 5 started at 1.000143066s 56 | // 6 started at 1.000144707s 57 | // 7 started at 2.000224641s 58 | // 8 started at 2.000240751s 59 | // 9 started at 2.00024244s 60 | // 10 started at 3.000314332s 61 | } 62 | ``` 63 | 64 | ### Blocking rate limiting with multiple limiters 65 | 66 | This example demonstrates combining rate limiters, one limiting at once per 67 | second, the other limiting at 2 times per 3 seconds. 68 | 69 | ```Go 70 | package main 71 | 72 | import ( 73 | "fmt" 74 | "time" 75 | 76 | "github.com/beefsack/go-rate" 77 | ) 78 | 79 | func main() { 80 | begin := time.Now() 81 | rl1 := rate.New(1, time.Second) // Once per second 82 | rl2 := rate.New(2, time.Second*3) // 2 times per 3 seconds 83 | for i := 1; i <= 10; i++ { 84 | rl1.Wait() 85 | rl2.Wait() 86 | fmt.Printf("%d started at %s\n", i, time.Now().Sub(begin)) 87 | } 88 | // Output: 89 | // 1 started at 11.197us 90 | // 2 started at 1.00011941s 91 | // 3 started at 3.000105858s 92 | // 4 started at 4.000210639s 93 | // 5 started at 6.000189578s 94 | // 6 started at 7.000289992s 95 | // 7 started at 9.000289942s 96 | // 8 started at 10.00038286s 97 | // 9 started at 12.000386821s 98 | // 10 started at 13.000465465s 99 | } 100 | ``` 101 | 102 | ### Non-blocking rate limiting 103 | 104 | This example demonstrates non-blocking rate limiting, such as would be used to 105 | limit spam in a chat client. 106 | 107 | ```Go 108 | package main 109 | 110 | import ( 111 | "fmt" 112 | "time" 113 | 114 | "github.com/beefsack/go-rate" 115 | ) 116 | 117 | var rl = rate.New(3, time.Second) // 3 times per second 118 | 119 | func say(message string) { 120 | if ok, remaining := rl.Try(); ok { 121 | fmt.Printf("You said: %s\n", message) 122 | } else { 123 | fmt.Printf("Spam filter triggered, please wait %s\n", remaining) 124 | } 125 | } 126 | 127 | func main() { 128 | for i := 1; i <= 5; i++ { 129 | say(fmt.Sprintf("Message %d", i)) 130 | } 131 | time.Sleep(time.Second / 2) 132 | say("I waited half a second, is that enough?") 133 | time.Sleep(time.Second / 2) 134 | say("Okay, I waited a second.") 135 | // Output: 136 | // You said: Message 1 137 | // You said: Message 2 138 | // You said: Message 3 139 | // Spam filter triggered, please wait 999.980816ms 140 | // Spam filter triggered, please wait 999.976704ms 141 | // Spam filter triggered, please wait 499.844795ms 142 | // You said: Okay, I waited a second. 143 | } 144 | ``` 145 | 146 | Authors 147 | ------- 148 | 149 | * [Michael Alexander](https://github.com/beefsack) 150 | * [Geert-Johan Riemer](https://github.com/GeertJohan) 151 | * [Matt T. Proud](https://github.com/matttproud) 152 | --------------------------------------------------------------------------------