├── .github ├── CONTRIBUTING.md └── workflows │ └── test.yml ├── LICENSE ├── Makefile ├── README.md ├── benchmarks ├── benchmarks_test.go ├── go.mod ├── go.sum ├── sethvargo_test.go ├── throttled_test.go ├── tollbooth_test.go ├── uber_test.go └── ulule_test.go ├── go.mod ├── httplimit ├── httplimit_test.go ├── middleware.go └── middleware_test.go ├── internal └── fasttime │ ├── fasttime.go │ └── fasttime_windows.go ├── limiter.go ├── memorystore ├── example_test.go ├── store.go └── store_test.go ├── noopstore ├── example_test.go └── store.go ├── store.go └── tools ├── go.mod ├── go.sum └── tools.go /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | 7 | ## Code reviews 8 | 9 | All submissions, including submissions by project members, require review. We 10 | use GitHub pull requests for this purpose. Consult 11 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 12 | information on using pull requests. 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: 'Test' 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - 'main' 12 | 13 | jobs: 14 | # unit runs the unit tests 15 | unit: 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: 20 | - 'macos-latest' 21 | - 'ubuntu-latest' 22 | - 'windows-latest' 23 | 24 | runs-on: '${{ matrix.os }}' 25 | 26 | steps: 27 | - uses: 'actions/checkout@v4' 28 | 29 | - uses: 'actions/setup-go@v5' 30 | with: 31 | go-version-file: 'go.mod' 32 | 33 | - name: Lint 34 | if: ${{ matrix.os == 'ubuntu-latest' }} 35 | run: |- 36 | make fmtcheck staticcheck spellcheck 37 | 38 | - name: Test 39 | run: |- 40 | make test-acc 41 | 42 | # build runs go build on the target platforms to ensure the runtime links are 43 | # correct. 44 | build: 45 | strategy: 46 | fail-fast: false 47 | matrix: 48 | goos: 49 | - 'darwin' 50 | - 'freebsd' 51 | - 'linux' 52 | - 'netbsd' 53 | - 'openbsd' 54 | - 'solaris' 55 | - 'windows' 56 | goarch: 57 | - 'amd64' 58 | 59 | runs-on: 'ubuntu-latest' 60 | 61 | steps: 62 | - uses: 'actions/checkout@v4' 63 | 64 | - uses: 'actions/setup-go@v5' 65 | with: 66 | go-version-file: 'go.mod' 67 | 68 | - name: Build 69 | env: 70 | GOOS: ${{ matrix.goos }} 71 | GOARCH: ${{ matrix.goarch }} 72 | run: |- 73 | go build -a ./... 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOFMT_FILES = $(shell go list -f '{{.Dir}}' ./...) 2 | 3 | benchmarks: 4 | @(cd benchmarks/ && go test -bench=. -benchmem -benchtime=1s ./...) 5 | .PHONY: benchmarks 6 | 7 | fmtcheck: 8 | @command -v goimports > /dev/null 2>&1 || (cd tools/ && go install golang.org/x/tools/cmd/goimports@latest) 9 | @CHANGES="$$(goimports -d $(GOFMT_FILES))"; \ 10 | if [ -n "$${CHANGES}" ]; then \ 11 | echo "Unformatted (run goimports -w .):\n\n$${CHANGES}\n\n"; \ 12 | exit 1; \ 13 | fi 14 | @# Annoyingly, goimports does not support the simplify flag. 15 | @CHANGES="$$(gofmt -s -d $(GOFMT_FILES))"; \ 16 | if [ -n "$${CHANGES}" ]; then \ 17 | echo "Unformatted (run gofmt -s -w .):\n\n$${CHANGES}\n\n"; \ 18 | exit 1; \ 19 | fi 20 | .PHONY: fmtcheck 21 | 22 | spellcheck: 23 | @command -v misspell > /dev/null 2>&1 || (cd tools/ && go install github.com/client9/misspell/cmd/misspell@latest) 24 | @misspell -locale="US" -error -source="text" **/* 25 | .PHONY: spellcheck 26 | 27 | staticcheck: 28 | @command -v staticcheck > /dev/null 2>&1 || (cd tools/ && go install honnef.co/go/tools/cmd/staticcheck@latest) 29 | @staticcheck -checks="all" -tests $(GOFMT_FILES) 30 | .PHONY: staticcheck 31 | 32 | test: 33 | @go test \ 34 | -count=1 \ 35 | -shuffle=on \ 36 | -short \ 37 | -timeout=5m \ 38 | -vet=all \ 39 | ./... 40 | .PHONY: test 41 | 42 | test-acc: 43 | @go test \ 44 | -count=1 \ 45 | -shuffle=on \ 46 | -race \ 47 | -timeout=10m \ 48 | -vet=all \ 49 | ./... 50 | .PHONY: test-acc 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Rate Limiter 2 | 3 | [![GoDoc](https://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/sethvargo/go-limiter) 4 | [![GitHub Actions](https://img.shields.io/github/actions/workflow/status/sethvargo/go-limiter/test.yml?style=flat-square)](https://github.com/sethvargo/go-limiter/actions/workflows/test.yml) 5 | 6 | 7 | This package provides a rate limiter in Go (Golang), suitable for use in HTTP 8 | servers and distributed workloads. It's specifically designed for 9 | configurability and flexibility without compromising throughput. 10 | 11 | 12 | ## Usage 13 | 14 | 1. Create a store. This example uses an in-memory store: 15 | 16 | ```golang 17 | store, err := memorystore.New(&memorystore.Config{ 18 | // Number of tokens allowed per interval. 19 | Tokens: 15, 20 | 21 | // Interval until tokens reset. 22 | Interval: time.Minute, 23 | }) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | ``` 28 | 29 | 1. Determine the limit by calling `Take()` on the store: 30 | 31 | ```golang 32 | ctx := context.Background() 33 | 34 | // key is the unique value upon which you want to rate limit, like an IP or 35 | // MAC address. 36 | key := "127.0.0.1" 37 | tokens, remaining, reset, ok, err := store.Take(ctx, key) 38 | 39 | // tokens is the configured tokens (15 in this example). 40 | _ = tokens 41 | 42 | // remaining is the number of tokens remaining (14 now). 43 | _ = remaining 44 | 45 | // reset is the unix nanoseconds at which the tokens will replenish. 46 | _ = reset 47 | 48 | // ok indicates whether the take was successful. If the key is over the 49 | // configured limit, ok will be false. 50 | _ = ok 51 | 52 | // Here's a more realistic example: 53 | if !ok { 54 | return fmt.Errorf("rate limited: retry at %v", reset) 55 | } 56 | ``` 57 | 58 | There's also HTTP middleware via the `httplimit` package. After creating a 59 | store, wrap Go's standard HTTP handler: 60 | 61 | ```golang 62 | middleware, err := httplimit.NewMiddleware(store, httplimit.IPKeyFunc()) 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | 67 | mux1 := http.NewServeMux() 68 | mux1.Handle("/", middleware.Handle(doWork)) // doWork is your original handler 69 | ``` 70 | 71 | The middleware automatically set the following headers, conforming to the latest 72 | RFCs: 73 | 74 | - `X-RateLimit-Limit` - configured rate limit (constant). 75 | - `X-RateLimit-Remaining` - number of remaining tokens in current interval. 76 | - `X-RateLimit-Reset` - UTC time when the limit resets. 77 | - `Retry-After` - Time at which to retry 78 | 79 | 80 | ## Why _another_ Go rate limiter? 81 | 82 | I really wanted to learn more about the topic and possibly implementations. The 83 | existing packages in the Go ecosystem either lacked flexibility or traded 84 | flexibility for performance. I wanted to write a package that was highly 85 | extensible while still offering the highest levels of performance. 86 | 87 | 88 | ### Speed and performance 89 | 90 | How fast is it? You can run the benchmarks yourself, but here's a few sample 91 | benchmarks with 100,000 unique keys. I added commas to the output for clarity, 92 | but you can run the benchmarks via `make benchmarks`: 93 | 94 | ```text 95 | $ make benchmarks 96 | BenchmarkSethVargoMemory/memory/serial-7 13,706,899 81.7 ns/op 16 B/op 1 allocs/op 97 | BenchmarkSethVargoMemory/memory/parallel-7 7,900,639 151 ns/op 61 B/op 3 allocs/op 98 | BenchmarkSethVargoMemory/sweep/serial-7 19,601,592 58.3 ns/op 0 B/op 0 allocs/op 99 | BenchmarkSethVargoMemory/sweep/parallel-7 21,042,513 55.2 ns/op 0 B/op 0 allocs/op 100 | BenchmarkThrottled/memory/serial-7 6,503,260 176 ns/op 0 B/op 0 allocs/op 101 | BenchmarkThrottled/memory/parallel-7 3,936,655 297 ns/op 0 B/op 0 allocs/op 102 | BenchmarkThrottled/sweep/serial-7 6,901,432 171 ns/op 0 B/op 0 allocs/op 103 | BenchmarkThrottled/sweep/parallel-7 5,948,437 202 ns/op 0 B/op 0 allocs/op 104 | BenchmarkTollbooth/memory/serial-7 3,064,309 368 ns/op 0 B/op 0 allocs/op 105 | BenchmarkTollbooth/memory/parallel-7 2,658,014 448 ns/op 0 B/op 0 allocs/op 106 | BenchmarkTollbooth/sweep/serial-7 2,769,937 430 ns/op 192 B/op 3 allocs/op 107 | BenchmarkTollbooth/sweep/parallel-7 2,216,211 546 ns/op 192 B/op 3 allocs/op 108 | BenchmarkUber/memory/serial-7 13,795,612 94.2 ns/op 0 B/op 0 allocs/op 109 | BenchmarkUber/memory/parallel-7 7,503,214 159 ns/op 0 B/op 0 allocs/op 110 | BenchmarkUlule/memory/serial-7 2,964,438 405 ns/op 24 B/op 2 allocs/op 111 | BenchmarkUlule/memory/parallel-7 2,441,778 469 ns/op 24 B/op 2 allocs/op 112 | ``` 113 | 114 | There's likely still optimizations to be had, pull requests are welcome! 115 | 116 | 117 | ### Ecosystem 118 | 119 | Many of the existing packages in the ecosystem take dependencies on other 120 | packages. I'm an advocate of very thin libraries, and I don't think a rate 121 | limiter should be pulling external packages. That's why **go-limit uses only the 122 | Go standard library**. 123 | 124 | 125 | ### Flexible and extensible 126 | 127 | Most of the existing rate limiting libraries make a strong assumption that rate 128 | limiting is only for HTTP services. Baked in that assumption are more 129 | assumptions like rate limiting by "IP address" or are limited to a resolution of 130 | "per second". While go-limit supports rate limiting at the HTTP layer, it can 131 | also be used to rate limit literally anything. It rate limits on a user-defined 132 | arbitrary string key. 133 | 134 | 135 | ### Stores 136 | 137 | #### Memory 138 | 139 | Memory is the fastest store, but only works on a single container/virtual 140 | machine since there's no way to share the state. 141 | [Learn more](https://pkg.go.dev/github.com/sethvargo/go-limiter/memorystore). 142 | 143 | #### Redis 144 | 145 | Redis uses Redis + Lua as a shared pool, but comes at a performance cost. 146 | [Learn more](https://pkg.go.dev/github.com/sethvargo/go-redisstore). 147 | 148 | #### Noop 149 | 150 | Noop does no rate limiting, but still implements the interface - useful for 151 | testing and local development. 152 | [Learn more](https://pkg.go.dev/github.com/sethvargo/go-limiter/noopstore). 153 | -------------------------------------------------------------------------------- /benchmarks/benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package benchmarks 2 | 3 | import ( 4 | "math/rand" 5 | "strconv" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | const ( 11 | // NumSessions is the number of unique sessions to store in the bucket. These 12 | // are pre-allocated in a map. 13 | NumSessions = 1000 14 | 15 | // SessionDefaultTokens is the default number of tokens to allow per interval. 16 | SessionDefaultTokens = 5 17 | 18 | // SessionDefaultInterval is the default ticking interval. 19 | SessionDefaultInterval = 500 * time.Millisecond 20 | 21 | // SessionMinTTL is the minimum amount of time a session should exist with no 22 | // requests before cleaning up. 23 | SessionMinTTL = 100 * time.Nanosecond 24 | 25 | // SessionSweepInterval is the frequency at which sessions should be swept and 26 | // purged. 27 | SessionSweepInterval = 1 * time.Millisecond 28 | ) 29 | 30 | var sessions map[int]string 31 | 32 | func init() { 33 | // Use math/rand since we don't actually need secure crypto here. 34 | rand.Seed(time.Now().UnixNano()) 35 | 36 | data := make([]string, NumSessions) 37 | for i := 0; i < NumSessions; i++ { 38 | data[i] = strconv.Itoa(rand.Int()) 39 | } 40 | } 41 | 42 | func testSessionID(tb testing.TB, i int) string { 43 | return sessions[i%NumSessions] 44 | } 45 | -------------------------------------------------------------------------------- /benchmarks/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sethvargo/go-limiter/benchmarks 2 | 3 | go 1.14 4 | 5 | replace github.com/sethvargo/go-limiter => ../ 6 | 7 | require ( 8 | github.com/didip/tollbooth/v6 v6.1.1 9 | github.com/gomodule/redigo v1.8.5 10 | github.com/hashicorp/golang-lru v0.5.4 // indirect 11 | github.com/sethvargo/go-limiter v0.6.0 12 | github.com/sethvargo/go-redisstore v0.3.0 13 | github.com/throttled/throttled v2.2.5+incompatible 14 | github.com/ulule/limiter/v3 v3.8.0 15 | go.uber.org/ratelimit v0.2.0 16 | ) 17 | -------------------------------------------------------------------------------- /benchmarks/go.sum: -------------------------------------------------------------------------------- 1 | github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= 2 | github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= 3 | github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 4 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 9 | github.com/didip/tollbooth/v6 v6.1.1 h1:Nt7PvWLa9Y94OrykXsFNBinVRQIu8xdy4avpl99Dc1M= 10 | github.com/didip/tollbooth/v6 v6.1.1/go.mod h1:xjcse6CTHCLuOkzsWrEgdy9WPJFv+p/x6v+MyfP+O9s= 11 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 12 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 13 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 14 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 15 | github.com/go-pkgz/expirable-cache v0.0.3 h1:rTh6qNPp78z0bQE6HDhXBHUwqnV9i09Vm6dksJLXQDc= 16 | github.com/go-pkgz/expirable-cache v0.0.3/go.mod h1:+IauqN00R2FqNRLCLA+X5YljQJrwB179PfiAoMPlTlQ= 17 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 18 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 19 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 20 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 21 | github.com/go-redis/redis/v8 v8.4.2/go.mod h1:A1tbYoHSa1fXwN+//ljcCYYJeLmVrwL9hbQN45Jdy0M= 22 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 23 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 24 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 25 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 26 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 27 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 28 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 29 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 30 | github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= 31 | github.com/gomodule/redigo v1.8.5 h1:nRAxCa+SVsyjSBrtZmG/cqb6VbTmuRzpg/PoTFlpumc= 32 | github.com/gomodule/redigo v1.8.5/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= 33 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 34 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 35 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 36 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 37 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 38 | github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= 39 | github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 40 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 41 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 42 | github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 43 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 44 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 45 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 46 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 47 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 48 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 49 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 50 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 51 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 52 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 53 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 54 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 55 | github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 56 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 57 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 58 | github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= 59 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 60 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 61 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 62 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 63 | github.com/sethvargo/go-redisstore v0.3.0 h1:yCDGc7ERWfa9BMgjhMhYcH8k+y85bRx0nziupGhjPkc= 64 | github.com/sethvargo/go-redisstore v0.3.0/go.mod h1:rY+FgiPpRrdpi4wETGHdMf6YlJnGiziAt2R8gXaFFxg= 65 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 66 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 67 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 68 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 69 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 70 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 71 | github.com/throttled/throttled v2.2.5+incompatible h1:65UB52X0qNTYiT0Sohp8qLYVFwZQPDw85uSa65OljjQ= 72 | github.com/throttled/throttled v2.2.5+incompatible/go.mod h1:0BjlrEGQmvxps+HuXLsyRdqpSRvJpq0PNIsOtqP9Nos= 73 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 74 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 75 | github.com/ulule/limiter/v3 v3.8.0 h1:rq76QxDIq5s/rvXc/A6HRHuGmehi/JE18qK3FaRUxKg= 76 | github.com/ulule/limiter/v3 v3.8.0/go.mod h1:TpV4HWgOM7M43mrkE7MU1S62/XtuoZ/C9PL+ExxeTK4= 77 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 78 | github.com/valyala/fasthttp v1.17.0/go.mod h1:jjraHZVbKOXftJfsOYoAjaeygpj5hr8ermTRJNroD7A= 79 | github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 80 | go.opentelemetry.io/otel v0.14.0/go.mod h1:vH5xEuwy7Rts0GNtsCW3HYQoZDY+OmBJ6t1bFGGlxgw= 81 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 82 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 83 | go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= 84 | go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= 85 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 86 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 87 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 88 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 89 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 90 | golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 91 | golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 92 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 93 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 94 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 95 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 96 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 97 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 98 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 99 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 100 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 101 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 102 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 103 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 104 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 105 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 106 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= 107 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 108 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 109 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 110 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 111 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 112 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 113 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 114 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 115 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 116 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 117 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 118 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 119 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 120 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 121 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 122 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 123 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 124 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 125 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 126 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 127 | -------------------------------------------------------------------------------- /benchmarks/sethvargo_test.go: -------------------------------------------------------------------------------- 1 | package benchmarks 2 | 3 | import ( 4 | "context" 5 | "math" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gomodule/redigo/redis" 11 | "github.com/sethvargo/go-limiter/memorystore" 12 | "github.com/sethvargo/go-redisstore" 13 | ) 14 | 15 | func BenchmarkSethVargoMemory(b *testing.B) { 16 | ctx := context.Background() 17 | 18 | cases := []struct { 19 | name string 20 | tokens uint64 21 | interval time.Duration 22 | sweepInterval time.Duration 23 | sweepMinTTL time.Duration 24 | }{ 25 | { 26 | name: "memory", 27 | tokens: math.MaxUint64, 28 | interval: time.Duration(math.MaxInt64), 29 | sweepInterval: time.Duration(math.MaxInt64), 30 | sweepMinTTL: time.Duration(math.MaxInt64), 31 | }, 32 | { 33 | name: "sweep", 34 | tokens: SessionDefaultTokens, 35 | interval: SessionDefaultInterval, 36 | sweepInterval: SessionSweepInterval, 37 | sweepMinTTL: SessionMinTTL, 38 | }, 39 | } 40 | 41 | for _, tc := range cases { 42 | tc := tc 43 | 44 | b.Run(tc.name, func(b *testing.B) { 45 | b.Run("serial", func(b *testing.B) { 46 | store, err := memorystore.New(&memorystore.Config{ 47 | SweepInterval: tc.sweepInterval, 48 | SweepMinTTL: tc.sweepMinTTL, 49 | Interval: tc.interval, 50 | Tokens: tc.tokens, 51 | }) 52 | if err != nil { 53 | b.Fatal(err) 54 | } 55 | b.Cleanup(func() { 56 | if err := store.Close(ctx); err != nil { 57 | b.Fatal(err) 58 | } 59 | }) 60 | b.ResetTimer() 61 | 62 | for i := 0; i < b.N; i++ { 63 | store.Take(ctx, testSessionID(b, i)) 64 | } 65 | b.StopTimer() 66 | }) 67 | 68 | b.Run("parallel", func(b *testing.B) { 69 | store, err := memorystore.New(&memorystore.Config{ 70 | SweepInterval: tc.sweepInterval, 71 | SweepMinTTL: tc.sweepMinTTL, 72 | Interval: tc.interval, 73 | Tokens: tc.tokens, 74 | }) 75 | if err != nil { 76 | b.Fatal(err) 77 | } 78 | b.Cleanup(func() { 79 | if err := store.Close(ctx); err != nil { 80 | b.Fatal(err) 81 | } 82 | }) 83 | b.ResetTimer() 84 | 85 | b.RunParallel(func(pb *testing.PB) { 86 | for i := 0; pb.Next(); i++ { 87 | store.Take(ctx, testSessionID(b, i)) 88 | } 89 | }) 90 | b.StopTimer() 91 | }) 92 | }) 93 | } 94 | } 95 | 96 | func BenchmarkSethVargoRedis(b *testing.B) { 97 | ctx := context.Background() 98 | 99 | host := os.Getenv("REDIS_HOST") 100 | if host == "" { 101 | b.Fatal("missing REDIS_HOST") 102 | } 103 | 104 | port := os.Getenv("REDIS_PORT") 105 | if port == "" { 106 | port = "6379" 107 | } 108 | 109 | pass := os.Getenv("REDIS_PASS") 110 | 111 | cases := []struct { 112 | name string 113 | tokens uint64 114 | interval time.Duration 115 | }{ 116 | { 117 | name: "redis", 118 | tokens: math.MaxUint64, 119 | interval: time.Duration(math.MaxInt64), 120 | }, 121 | } 122 | 123 | for _, tc := range cases { 124 | tc := tc 125 | 126 | b.Run(tc.name, func(b *testing.B) { 127 | b.Run("serial", func(b *testing.B) { 128 | 129 | store, err := redisstore.New(&redisstore.Config{ 130 | Tokens: tc.tokens, 131 | Interval: tc.interval, 132 | Dial: func() (redis.Conn, error) { 133 | return redis.Dial("tcp", host+":"+port, 134 | redis.DialPassword(pass)) 135 | }, 136 | }) 137 | if err != nil { 138 | b.Fatal(err) 139 | } 140 | b.Cleanup(func() { 141 | if err := store.Close(ctx); err != nil { 142 | b.Fatal(err) 143 | } 144 | }) 145 | b.ResetTimer() 146 | 147 | for i := 0; i < b.N; i++ { 148 | store.Take(ctx, testSessionID(b, i)) 149 | } 150 | b.StopTimer() 151 | }) 152 | 153 | b.Run("parallel", func(b *testing.B) { 154 | 155 | store, err := redisstore.New(&redisstore.Config{ 156 | Tokens: tc.tokens, 157 | Interval: tc.interval, 158 | Dial: func() (redis.Conn, error) { 159 | return redis.Dial("tcp", host+":"+port, 160 | redis.DialPassword(pass)) 161 | }, 162 | }) 163 | if err != nil { 164 | b.Fatal(err) 165 | } 166 | b.Cleanup(func() { 167 | if err := store.Close(ctx); err != nil { 168 | b.Fatal(err) 169 | } 170 | }) 171 | b.ResetTimer() 172 | 173 | b.RunParallel(func(pb *testing.PB) { 174 | for i := 0; pb.Next(); i++ { 175 | store.Take(ctx, testSessionID(b, i)) 176 | } 177 | }) 178 | b.StopTimer() 179 | }) 180 | }) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /benchmarks/throttled_test.go: -------------------------------------------------------------------------------- 1 | package benchmarks 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/throttled/throttled" 7 | "github.com/throttled/throttled/store/memstore" 8 | ) 9 | 10 | func BenchmarkThrottled(b *testing.B) { 11 | cases := []struct { 12 | name string 13 | tokens int 14 | }{ 15 | { 16 | name: "memory", 17 | tokens: 1000000000, // throttled overflows larger than this 18 | }, 19 | { 20 | name: "sweep", 21 | tokens: SessionDefaultTokens, 22 | }, 23 | } 24 | 25 | for _, tc := range cases { 26 | tc := tc 27 | 28 | b.Run(tc.name, func(b *testing.B) { 29 | b.Run("serial", func(b *testing.B) { 30 | store, err := memstore.New(4096) 31 | if err != nil { 32 | b.Fatal(err) 33 | } 34 | 35 | quota := throttled.RateQuota{MaxRate: throttled.PerSec(tc.tokens)} 36 | limiter, err := throttled.NewGCRARateLimiter(store, quota) 37 | if err != nil { 38 | b.Fatal(err) 39 | } 40 | b.ResetTimer() 41 | 42 | for i := 0; i < b.N; i++ { 43 | limiter.RateLimit(testSessionID(b, i), 1) 44 | } 45 | }) 46 | 47 | b.Run("parallel", func(b *testing.B) { 48 | store, err := memstore.New(4096) 49 | if err != nil { 50 | b.Fatal(err) 51 | } 52 | 53 | quota := throttled.RateQuota{MaxRate: throttled.PerSec(tc.tokens)} 54 | limiter, err := throttled.NewGCRARateLimiter(store, quota) 55 | if err != nil { 56 | b.Fatal(err) 57 | } 58 | b.ResetTimer() 59 | 60 | b.RunParallel(func(pb *testing.PB) { 61 | for i := 0; pb.Next(); i++ { 62 | limiter.RateLimit(testSessionID(b, i), 1) 63 | } 64 | }) 65 | }) 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /benchmarks/tollbooth_test.go: -------------------------------------------------------------------------------- 1 | package benchmarks 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | "time" 7 | 8 | "github.com/didip/tollbooth/v6/limiter" 9 | ) 10 | 11 | func BenchmarkTollbooth(b *testing.B) { 12 | cases := []struct { 13 | name string 14 | tokens uint64 15 | sweepMinTTL time.Duration 16 | }{ 17 | { 18 | name: "memory", 19 | tokens: math.MaxUint64, 20 | sweepMinTTL: time.Duration(math.MaxInt64), 21 | }, 22 | { 23 | name: "sweep", 24 | tokens: SessionDefaultTokens, 25 | sweepMinTTL: SessionMinTTL, 26 | }, 27 | } 28 | 29 | for _, tc := range cases { 30 | tc := tc 31 | 32 | b.Run(tc.name, func(b *testing.B) { 33 | b.Run("serial", func(b *testing.B) { 34 | // Note: tollbooth doesn't support any granularity lower than 1 second 35 | instance := limiter.New(nil). 36 | SetMax(float64(tc.tokens)). 37 | SetTokenBucketExpirationTTL(tc.sweepMinTTL) 38 | b.ResetTimer() 39 | 40 | for i := 0; i < b.N; i++ { 41 | instance.LimitReached(testSessionID(b, i)) 42 | } 43 | }) 44 | 45 | b.Run("parallel", func(b *testing.B) { 46 | // Note: tollbooth doesn't support any granularity lower than 1 second 47 | instance := limiter.New(nil). 48 | SetMax(float64(tc.tokens)). 49 | SetTokenBucketExpirationTTL(tc.sweepMinTTL) 50 | b.ResetTimer() 51 | 52 | b.RunParallel(func(pb *testing.PB) { 53 | for i := 0; pb.Next(); i++ { 54 | instance.LimitReached(testSessionID(b, i)) 55 | } 56 | }) 57 | }) 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /benchmarks/uber_test.go: -------------------------------------------------------------------------------- 1 | package benchmarks 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "go.uber.org/ratelimit" 8 | ) 9 | 10 | func BenchmarkUber(b *testing.B) { 11 | cases := []struct { 12 | name string 13 | tokens int 14 | }{ 15 | { 16 | name: "memory", 17 | tokens: math.MaxInt64, 18 | }, 19 | } 20 | 21 | for _, tc := range cases { 22 | tc := tc 23 | 24 | b.Run(tc.name, func(b *testing.B) { 25 | b.Run("serial", func(b *testing.B) { 26 | instance := ratelimit.New(int(tc.tokens)) 27 | b.ResetTimer() 28 | 29 | var x string 30 | for i := 0; i < b.N; i++ { 31 | x = testSessionID(b, i) 32 | instance.Take() 33 | } 34 | _ = x 35 | }) 36 | 37 | b.Run("parallel", func(b *testing.B) { 38 | instance := ratelimit.New(int(tc.tokens)) 39 | b.ResetTimer() 40 | 41 | var x string 42 | b.RunParallel(func(pb *testing.PB) { 43 | for i := 0; pb.Next(); i++ { 44 | x = testSessionID(b, i) 45 | instance.Take() 46 | } 47 | }) 48 | _ = x 49 | }) 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /benchmarks/ulule_test.go: -------------------------------------------------------------------------------- 1 | package benchmarks 2 | 3 | import ( 4 | "context" 5 | "math" 6 | "testing" 7 | "time" 8 | 9 | "github.com/ulule/limiter/v3" 10 | "github.com/ulule/limiter/v3/drivers/store/memory" 11 | ) 12 | 13 | func BenchmarkUlule(b *testing.B) { 14 | cases := []struct { 15 | name string 16 | tokens uint64 17 | interval time.Duration 18 | }{ 19 | { 20 | name: "memory", 21 | tokens: math.MaxUint64, 22 | interval: time.Duration(math.MaxInt64), 23 | }, 24 | } 25 | 26 | for _, tc := range cases { 27 | tc := tc 28 | 29 | b.Run(tc.name, func(b *testing.B) { 30 | b.Run("serial", func(b *testing.B) { 31 | ctx := context.Background() 32 | store := memory.NewStore() 33 | instance := limiter.New(store, limiter.Rate{ 34 | Limit: int64(tc.tokens), 35 | Period: tc.interval, 36 | }) 37 | b.ResetTimer() 38 | 39 | for i := 0; i < b.N; i++ { 40 | instance.Get(ctx, testSessionID(b, i)) 41 | } 42 | }) 43 | 44 | b.Run("parallel", func(b *testing.B) { 45 | ctx := context.Background() 46 | store := memory.NewStore() 47 | instance := limiter.New(store, limiter.Rate{ 48 | Limit: int64(tc.tokens), 49 | Period: tc.interval, 50 | }) 51 | b.ResetTimer() 52 | 53 | b.RunParallel(func(pb *testing.PB) { 54 | for i := 0; pb.Next(); i++ { 55 | instance.Get(ctx, testSessionID(b, i)) 56 | } 57 | }) 58 | }) 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sethvargo/go-limiter 2 | 3 | go 1.22 4 | 5 | toolchain go1.22.1 6 | -------------------------------------------------------------------------------- /httplimit/httplimit_test.go: -------------------------------------------------------------------------------- 1 | package httplimit_test 2 | 3 | import ( 4 | "crypto/sha512" 5 | "encoding/base64" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/sethvargo/go-limiter/httplimit" 12 | "github.com/sethvargo/go-limiter/memorystore" 13 | ) 14 | 15 | var keyFunc httplimit.KeyFunc 16 | 17 | func ExampleKeyFunc_custom() { 18 | // This is an example KeyFunc that rate limits using the value from the 19 | // X-API-Key header. Since this value is likely a secret, it is hashed before 20 | // passing along to the store. 21 | keyFunc = httplimit.KeyFunc(func(r *http.Request) (string, error) { 22 | dig := sha512.Sum512([]byte(r.Header.Get("X-Token"))) 23 | return base64.StdEncoding.EncodeToString(dig[:]), nil 24 | }) 25 | // middleware, err := httplimit.NewMiddleware(store, keyFunc) 26 | } 27 | 28 | func ExampleIPKeyFunc_headers() { 29 | keyFunc = httplimit.IPKeyFunc("X-Forwarded-For") 30 | // middleware, err := httplimit.NewMiddleware(store, keyFunc) 31 | } 32 | 33 | func ExampleNewMiddleware() { 34 | // Create a store that allows 30 requests per minute. 35 | store, err := memorystore.New(&memorystore.Config{ 36 | Tokens: 30, 37 | Interval: time.Minute, 38 | }) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | // Create the HTTP middleware from the store, keying by IP address. 44 | middleware, err := httplimit.NewMiddleware(store, httplimit.IPKeyFunc()) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | doWork := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 50 | w.WriteHeader(200) 51 | fmt.Fprintf(w, "hello world") 52 | }) 53 | 54 | // Wrap an individual handler (only rate limits this endpoint). 55 | mux1 := http.NewServeMux() 56 | mux1.Handle("/foo", middleware.Handle(doWork)) // rate limited 57 | mux1.Handle("/bar", doWork) // not rate limited 58 | _ = mux1 59 | 60 | // Or wrap the entire mux (rate limits all endpoints). 61 | mux2 := http.NewServeMux() 62 | mux2.Handle("/foo", doWork) 63 | mux2.Handle("/bar", doWork) 64 | router := middleware.Handle(mux2) // all endpoints are rate limited 65 | _ = router 66 | } 67 | -------------------------------------------------------------------------------- /httplimit/middleware.go: -------------------------------------------------------------------------------- 1 | // Package httplimit provides middleware for rate limiting HTTP handlers. 2 | // 3 | // The implementation is designed to work with Go's built-in http.Handler and 4 | // http.HandlerFunc interfaces, so it will also work with any popular web 5 | // frameworks that support middleware with these properties. 6 | package httplimit 7 | 8 | import ( 9 | "fmt" 10 | "net" 11 | "net/http" 12 | "strconv" 13 | "time" 14 | 15 | "github.com/sethvargo/go-limiter" 16 | ) 17 | 18 | const ( 19 | // HeaderRateLimitLimit, HeaderRateLimitRemaining, and HeaderRateLimitReset 20 | // are the recommended return header values from IETF on rate limiting. Reset 21 | // is in UTC time. 22 | HeaderRateLimitLimit = "X-RateLimit-Limit" 23 | HeaderRateLimitRemaining = "X-RateLimit-Remaining" 24 | HeaderRateLimitReset = "X-RateLimit-Reset" 25 | 26 | // HeaderRetryAfter is the header used to indicate when a client should retry 27 | // requests (when the rate limit expires), in UTC time. 28 | HeaderRetryAfter = "Retry-After" 29 | ) 30 | 31 | // KeyFunc is a function that accepts an http request and returns a string key 32 | // that uniquely identifies this request for the purpose of rate limiting. 33 | // 34 | // KeyFuncs are called on each request, so be mindful of performance and 35 | // implement caching where possible. If a KeyFunc returns an error, the HTTP 36 | // handler will return Internal Server Error and will NOT take from the limiter 37 | // store. 38 | type KeyFunc func(r *http.Request) (string, error) 39 | 40 | // IPKeyFunc returns a function that keys data based on the incoming requests IP 41 | // address. By default this uses the RemoteAddr, but you can also specify a list 42 | // of headers which will be checked for an IP address first (e.g. 43 | // "X-Forwarded-For"). Headers are retrieved using Header.Get(), which means 44 | // they are case insensitive. 45 | func IPKeyFunc(headers ...string) KeyFunc { 46 | return func(r *http.Request) (string, error) { 47 | for _, h := range headers { 48 | if v := r.Header.Get(h); v != "" { 49 | return v, nil 50 | } 51 | } 52 | 53 | ip, _, err := net.SplitHostPort(r.RemoteAddr) 54 | if err != nil { 55 | return "", err 56 | } 57 | return ip, nil 58 | } 59 | } 60 | 61 | // Middleware is a handler/mux that can wrap other middlware to implement HTTP 62 | // rate limiting. It can rate limit based on an arbitrary KeyFunc, and supports 63 | // anything that implements limiter.StoreWithContext. 64 | type Middleware struct { 65 | store limiter.Store 66 | keyFunc KeyFunc 67 | } 68 | 69 | // NewMiddleware creates a new middleware suitable for use as an HTTP handler. 70 | // This function returns an error if either the Store or KeyFunc are nil. 71 | func NewMiddleware(s limiter.Store, f KeyFunc) (*Middleware, error) { 72 | if s == nil { 73 | return nil, fmt.Errorf("store cannot be nil") 74 | } 75 | 76 | if f == nil { 77 | return nil, fmt.Errorf("key function cannot be nil") 78 | } 79 | 80 | return &Middleware{ 81 | store: s, 82 | keyFunc: f, 83 | }, nil 84 | } 85 | 86 | // Handle returns the HTTP handler as a middleware. This handler calls Take() on 87 | // the store and sets the common rate limiting headers. If the take is 88 | // successful, the remaining middleware is called. If take is unsuccessful, the 89 | // middleware chain is halted and the function renders a 429 to the caller with 90 | // metadata about when it's safe to retry. 91 | func (m *Middleware) Handle(next http.Handler) http.Handler { 92 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 93 | ctx := r.Context() 94 | 95 | // Call the key function - if this fails, it's an internal server error. 96 | key, err := m.keyFunc(r) 97 | if err != nil { 98 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 99 | return 100 | } 101 | 102 | // Take from the store. 103 | limit, remaining, reset, ok, err := m.store.Take(ctx, key) 104 | if err != nil { 105 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 106 | return 107 | } 108 | 109 | resetTime := time.Unix(0, int64(reset)).UTC().Format(time.RFC1123) 110 | 111 | // Set headers (we do this regardless of whether the request is permitted). 112 | w.Header().Set(HeaderRateLimitLimit, strconv.FormatUint(limit, 10)) 113 | w.Header().Set(HeaderRateLimitRemaining, strconv.FormatUint(remaining, 10)) 114 | w.Header().Set(HeaderRateLimitReset, resetTime) 115 | 116 | // Fail if there were no tokens remaining. 117 | if !ok { 118 | w.Header().Set(HeaderRetryAfter, resetTime) 119 | http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests) 120 | return 121 | } 122 | 123 | // If we got this far, we're allowed to continue, so call the next middleware 124 | // in the stack to continue processing. 125 | next.ServeHTTP(w, r) 126 | }) 127 | } 128 | -------------------------------------------------------------------------------- /httplimit/middleware_test.go: -------------------------------------------------------------------------------- 1 | package httplimit_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "strconv" 8 | "testing" 9 | "time" 10 | 11 | "github.com/sethvargo/go-limiter/httplimit" 12 | "github.com/sethvargo/go-limiter/memorystore" 13 | ) 14 | 15 | func TestNewMiddleware(t *testing.T) { 16 | t.Parallel() 17 | 18 | cases := []struct { 19 | name string 20 | tokens uint64 21 | interval time.Duration 22 | }{ 23 | { 24 | name: "millisecond", 25 | tokens: 5, 26 | interval: 500 * time.Millisecond, 27 | }, 28 | { 29 | name: "second", 30 | tokens: 3, 31 | interval: time.Second, 32 | }, 33 | } 34 | 35 | for _, tc := range cases { 36 | tc := tc 37 | 38 | t.Run(tc.name, func(t *testing.T) { 39 | t.Parallel() 40 | 41 | store, err := memorystore.New(&memorystore.Config{ 42 | Tokens: tc.tokens, 43 | Interval: tc.interval, 44 | }) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | middleware, err := httplimit.NewMiddleware(store, httplimit.IPKeyFunc()) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | doWork := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 55 | w.WriteHeader(200) 56 | fmt.Fprintf(w, "hello world") 57 | }) 58 | 59 | server := httptest.NewServer(middleware.Handle(doWork)) 60 | defer server.Close() 61 | 62 | client := server.Client() 63 | 64 | for i := uint64(0); i < tc.tokens; i++ { 65 | resp, err := client.Get(server.URL) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | limit, err := strconv.ParseUint(resp.Header.Get(httplimit.HeaderRateLimitLimit), 10, 64) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | if got, want := limit, tc.tokens; got != want { 75 | t.Errorf("limit: expected %d to be %d", got, want) 76 | } 77 | 78 | reset, err := time.Parse(time.RFC1123, resp.Header.Get(httplimit.HeaderRateLimitReset)) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | if got, want := time.Until(reset), tc.interval; got > want { 83 | t.Errorf("reset: expected %d to be less than %d", got, want) 84 | } 85 | 86 | remaining, err := strconv.ParseUint(resp.Header.Get(httplimit.HeaderRateLimitRemaining), 10, 64) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | if got, want := remaining, tc.tokens-uint64(i)-1; got != want { 91 | t.Errorf("remaining: expected %d to be %d", got, want) 92 | } 93 | } 94 | 95 | // Should be limited 96 | resp, err := client.Get(server.URL) 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | if got, want := resp.StatusCode, http.StatusTooManyRequests; got != want { 101 | t.Errorf("expected %d to be %d", got, want) 102 | } 103 | 104 | limit, err := strconv.ParseUint(resp.Header.Get(httplimit.HeaderRateLimitLimit), 10, 64) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | if got, want := limit, tc.tokens; got != want { 109 | t.Errorf("limit: expected %d to be %d", got, want) 110 | } 111 | 112 | reset, err := time.Parse(time.RFC1123, resp.Header.Get(httplimit.HeaderRateLimitReset)) 113 | if err != nil { 114 | t.Fatal(err) 115 | } 116 | if got, want := time.Until(reset), tc.interval; got > want { 117 | t.Errorf("reset: expected %d to be less than %d", got, want) 118 | } 119 | 120 | remaining, err := strconv.ParseUint(resp.Header.Get(httplimit.HeaderRateLimitRemaining), 10, 64) 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | if got, want := remaining, uint64(0); got != want { 125 | t.Errorf("remaining: expected %d to be %d", got, want) 126 | } 127 | }) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /internal/fasttime/fasttime.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | // Package fasttime gets wallclock time, but super fast. 4 | package fasttime 5 | 6 | import ( 7 | _ "unsafe" 8 | ) 9 | 10 | //go:noescape 11 | //go:linkname now time.now 12 | func now() (sec int64, nsec int32, mono int64) 13 | 14 | // Now returns a monotonic and wall clock value. The actual value will differ 15 | // across systems, but that's okay because we generally only care about the 16 | // deltas. 17 | func Now() uint64 { 18 | sec, nsec, _ := now() 19 | return uint64(sec)*1e9 + uint64(nsec) 20 | } 21 | -------------------------------------------------------------------------------- /internal/fasttime/fasttime_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package fasttime 4 | 5 | import "time" 6 | 7 | // Now returns a monotonic clock value. On Windows, no such clock exists, so we 8 | // fallback to time.Now(). 9 | func Now() uint64 { 10 | return uint64(time.Now().UnixNano()) 11 | } 12 | -------------------------------------------------------------------------------- /limiter.go: -------------------------------------------------------------------------------- 1 | // Package limiter defines rate limiting systems. 2 | package limiter 3 | -------------------------------------------------------------------------------- /memorystore/example_test.go: -------------------------------------------------------------------------------- 1 | package memorystore_test 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/sethvargo/go-limiter/memorystore" 9 | ) 10 | 11 | func ExampleNew() { 12 | ctx := context.Background() 13 | 14 | store, err := memorystore.New(&memorystore.Config{ 15 | Tokens: 15, 16 | Interval: time.Minute, 17 | }) 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | defer store.Close(ctx) 22 | 23 | limit, remaining, reset, ok, err := store.Take(ctx, "my-key") 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | _, _, _, _ = limit, remaining, reset, ok 28 | } 29 | -------------------------------------------------------------------------------- /memorystore/store.go: -------------------------------------------------------------------------------- 1 | // Package memorystore defines an in-memory storage system for limiting. 2 | package memorystore 3 | 4 | import ( 5 | "context" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/sethvargo/go-limiter" 11 | "github.com/sethvargo/go-limiter/internal/fasttime" 12 | ) 13 | 14 | var _ limiter.Store = (*store)(nil) 15 | 16 | type store struct { 17 | tokens uint64 18 | interval time.Duration 19 | 20 | sweepInterval time.Duration 21 | sweepMinTTL uint64 22 | 23 | data map[string]*bucket 24 | dataLock sync.RWMutex 25 | 26 | stopped uint32 27 | stopCh chan struct{} 28 | } 29 | 30 | // Config is used as input to New. It defines the behavior of the storage 31 | // system. 32 | type Config struct { 33 | // Tokens is the number of tokens to allow per interval. The default value is 34 | // 1. 35 | Tokens uint64 36 | 37 | // Interval is the time interval upon which to enforce rate limiting. The 38 | // default value is 1 second. 39 | Interval time.Duration 40 | 41 | // SweepInterval is the rate at which to run the garabage collection on stale 42 | // entries. Setting this to a low value will optimize memory consumption, but 43 | // will likely reduce performance and increase lock contention. Setting this 44 | // to a high value will maximum throughput, but will increase the memory 45 | // footprint. This can be tuned in combination with SweepMinTTL to control how 46 | // long stale entires are kept. The default value is 6 hours. 47 | SweepInterval time.Duration 48 | 49 | // SweepMinTTL is the minimum amount of time a session must be inactive before 50 | // clearing it from the entries. There's no validation, but this should be at 51 | // least as high as your rate limit, or else the data store will purge records 52 | // before they limit is applied. The default value is 12 hours. 53 | SweepMinTTL time.Duration 54 | 55 | // InitialAlloc is the size to use for the in-memory map. Go will 56 | // automatically expand the buffer, but choosing higher number can trade 57 | // memory consumption for performance as it limits the number of times the map 58 | // needs to expand. The default value is 4096. 59 | InitialAlloc int 60 | } 61 | 62 | // New creates an in-memory rate limiter that uses a bucketing model to limit 63 | // the number of permitted events over an interval. It's optimized for runtime 64 | // and memory efficiency. 65 | func New(c *Config) (limiter.Store, error) { 66 | if c == nil { 67 | c = new(Config) 68 | } 69 | 70 | tokens := uint64(1) 71 | if c.Tokens > 0 { 72 | tokens = c.Tokens 73 | } 74 | 75 | interval := 1 * time.Second 76 | if c.Interval > 0 { 77 | interval = c.Interval 78 | } 79 | 80 | sweepInterval := 6 * time.Hour 81 | if c.SweepInterval > 0 { 82 | sweepInterval = c.SweepInterval 83 | } 84 | 85 | sweepMinTTL := 12 * time.Hour 86 | if c.SweepMinTTL > 0 { 87 | sweepMinTTL = c.SweepMinTTL 88 | } 89 | 90 | initialAlloc := 4096 91 | if c.InitialAlloc > 0 { 92 | initialAlloc = c.InitialAlloc 93 | } 94 | 95 | s := &store{ 96 | tokens: tokens, 97 | interval: interval, 98 | 99 | sweepInterval: sweepInterval, 100 | sweepMinTTL: uint64(sweepMinTTL), 101 | 102 | data: make(map[string]*bucket, initialAlloc), 103 | stopCh: make(chan struct{}), 104 | } 105 | go s.purge() 106 | return s, nil 107 | } 108 | 109 | // Take attempts to remove a token from the named key. If the take is 110 | // successful, it returns true, otherwise false. It also returns the configured 111 | // limit, remaining tokens, and reset time. 112 | func (s *store) Take(ctx context.Context, key string) (uint64, uint64, uint64, bool, error) { 113 | // If the store is stopped, all requests are rejected. 114 | if atomic.LoadUint32(&s.stopped) == 1 { 115 | return 0, 0, 0, false, limiter.ErrStopped 116 | } 117 | 118 | // Acquire a read lock first - this allows other to concurrently check limits 119 | // without taking a full lock. 120 | s.dataLock.RLock() 121 | if b, ok := s.data[key]; ok { 122 | s.dataLock.RUnlock() 123 | return b.take() 124 | } 125 | s.dataLock.RUnlock() 126 | 127 | // Unfortunately we did not find the key in the map. Take out a full lock. We 128 | // have to check if the key exists again, because it's possible another 129 | // goroutine created it between our shared lock and exclusive lock. 130 | s.dataLock.Lock() 131 | if b, ok := s.data[key]; ok { 132 | s.dataLock.Unlock() 133 | return b.take() 134 | } 135 | 136 | // This is the first time we've seen this entry (or it's been garbage 137 | // collected), so create the bucket and take an initial request. 138 | b := newBucket(s.tokens, s.interval) 139 | 140 | // Add it to the map and take. 141 | s.data[key] = b 142 | s.dataLock.Unlock() 143 | return b.take() 144 | } 145 | 146 | // Get retrieves the information about the key, if any exists. 147 | func (s *store) Get(ctx context.Context, key string) (uint64, uint64, error) { 148 | // If the store is stopped, all requests are rejected. 149 | if atomic.LoadUint32(&s.stopped) == 1 { 150 | return 0, 0, limiter.ErrStopped 151 | } 152 | 153 | // Acquire a read lock first - this allows other to concurrently check limits 154 | // without taking a full lock. 155 | s.dataLock.RLock() 156 | if b, ok := s.data[key]; ok { 157 | s.dataLock.RUnlock() 158 | return b.get() 159 | } 160 | s.dataLock.RUnlock() 161 | 162 | return 0, 0, nil 163 | } 164 | 165 | // Set configures the bucket-specific tokens and interval. 166 | func (s *store) Set(ctx context.Context, key string, tokens uint64, interval time.Duration) error { 167 | s.dataLock.Lock() 168 | b := newBucket(tokens, interval) 169 | s.data[key] = b 170 | s.dataLock.Unlock() 171 | return nil 172 | } 173 | 174 | // Burst adds the provided value to the bucket's currently available tokens. 175 | func (s *store) Burst(ctx context.Context, key string, tokens uint64) error { 176 | s.dataLock.RLock() 177 | if b, ok := s.data[key]; ok { 178 | s.dataLock.RUnlock() 179 | b.burst(tokens) 180 | return nil 181 | } 182 | s.dataLock.RUnlock() 183 | 184 | s.dataLock.Lock() 185 | // check again just in case 186 | if b, ok := s.data[key]; ok { 187 | s.dataLock.Unlock() 188 | b.burst(tokens) 189 | return nil 190 | } 191 | 192 | // If we got this far, there's no current record for the key. 193 | b := newBucket(s.tokens+tokens, s.interval) 194 | s.data[key] = b 195 | s.dataLock.Unlock() 196 | return nil 197 | } 198 | 199 | // Close stops the memory limiter and cleans up any outstanding 200 | // sessions. You should always call Close() as it releases the memory consumed 201 | // by the map AND releases the tickers. 202 | func (s *store) Close(ctx context.Context) error { 203 | if !atomic.CompareAndSwapUint32(&s.stopped, 0, 1) { 204 | return nil 205 | } 206 | 207 | // Close the channel to prevent future purging. 208 | close(s.stopCh) 209 | 210 | // Delete all the things. 211 | s.dataLock.Lock() 212 | for k := range s.data { 213 | delete(s.data, k) 214 | } 215 | s.dataLock.Unlock() 216 | return nil 217 | } 218 | 219 | // purge continually iterates over the map and purges old values on the provided 220 | // sweep interval. Earlier designs used a go-function-per-item expiration, but 221 | // it actually generated *more* lock contention under normal use. The most 222 | // performant option with real-world data was a global garbage collection on a 223 | // fixed interval. 224 | func (s *store) purge() { 225 | ticker := time.NewTicker(s.sweepInterval) 226 | defer ticker.Stop() 227 | 228 | for { 229 | select { 230 | case <-s.stopCh: 231 | return 232 | case <-ticker.C: 233 | } 234 | 235 | s.dataLock.RLock() 236 | now := fasttime.Now() 237 | var deletes []string 238 | for k, b := range s.data { 239 | b.lock.RLock() 240 | lastTime := b.startTime + (b.lastTick * uint64(b.interval)) 241 | b.lock.RUnlock() 242 | 243 | if now-lastTime > s.sweepMinTTL { 244 | deletes = append(deletes, k) 245 | } 246 | } 247 | s.dataLock.RUnlock() 248 | 249 | for _, k := range deletes { 250 | s.dataLock.Lock() 251 | delete(s.data, k) 252 | s.dataLock.Unlock() 253 | } 254 | } 255 | } 256 | 257 | // bucket is an internal wrapper around a taker. 258 | type bucket struct { 259 | // startTime is the number of nanoseconds from unix epoch when this bucket was 260 | // initially created. 261 | startTime uint64 262 | 263 | // maxTokens is the maximum number of tokens permitted on the bucket at any 264 | // time. The number of available tokens will never exceed this value. 265 | maxTokens uint64 266 | 267 | // interval is the time at which ticking should occur. 268 | interval time.Duration 269 | 270 | // availableTokens is the current point-in-time number of tokens remaining. 271 | availableTokens uint64 272 | 273 | // lastTick is the last clock tick, used to re-calculate the number of tokens 274 | // on the bucket. 275 | lastTick uint64 276 | 277 | // lock guards the mutable fields. 278 | lock sync.RWMutex 279 | } 280 | 281 | // newBucket creates a new bucket from the given tokens and interval. 282 | func newBucket(tokens uint64, interval time.Duration) *bucket { 283 | b := &bucket{ 284 | startTime: fasttime.Now(), 285 | maxTokens: tokens, 286 | availableTokens: tokens, 287 | interval: interval, 288 | } 289 | return b 290 | } 291 | 292 | // get returns information about the bucket. 293 | func (b *bucket) get() (tokens uint64, remaining uint64, retErr error) { 294 | b.lock.RLock() 295 | defer b.lock.RUnlock() 296 | 297 | tokens = b.maxTokens 298 | remaining = b.availableTokens 299 | return 300 | } 301 | 302 | // take attempts to remove a token from the bucket. If there are no tokens 303 | // available and the clock has ticked forward, it recalculates the number of 304 | // tokens and retries. It returns the limit, remaining tokens, time until 305 | // refresh, and whether the take was successful. 306 | func (b *bucket) take() (tokens uint64, remaining uint64, reset uint64, ok bool, retErr error) { 307 | // Capture the current request time, current tick, and amount of time until 308 | // the bucket resets. 309 | now := fasttime.Now() 310 | 311 | b.lock.Lock() 312 | defer b.lock.Unlock() 313 | 314 | // If the current time is before the start time, it means the server clock was 315 | // reset to an earlier time. In that case, rebase to 0. 316 | if now < b.startTime { 317 | b.startTime = now 318 | b.lastTick = 0 319 | } 320 | 321 | currTick := tick(b.startTime, now, b.interval) 322 | 323 | tokens = b.maxTokens 324 | reset = b.startTime + ((currTick + 1) * uint64(b.interval)) 325 | 326 | // If we're on a new tick since last assessment, perform 327 | // a full reset up to maxTokens. 328 | if b.lastTick < currTick { 329 | b.availableTokens = b.maxTokens 330 | b.lastTick = currTick 331 | } 332 | 333 | if b.availableTokens > 0 { 334 | b.availableTokens-- 335 | ok = true 336 | remaining = b.availableTokens 337 | } 338 | 339 | return 340 | } 341 | 342 | // burst adds the specified number of tokens to the bucket's available tokens in a thread-safe manner. 343 | func (b *bucket) burst(tokens uint64) { 344 | b.lock.Lock() 345 | b.availableTokens = b.availableTokens + tokens 346 | b.lock.Unlock() 347 | } 348 | 349 | // tick is the total number of times the current interval has occurred between 350 | // when the time started (start) and the current time (curr). For example, if 351 | // the start time was 12:30pm and it's currently 1:00pm, and the interval was 5 352 | // minutes, tick would return 6 because 1:00pm is the 6th 5-minute tick. Note 353 | // that tick would return 5 at 12:59pm, because it hasn't reached the 6th tick 354 | // yet. 355 | func tick(start, curr uint64, interval time.Duration) uint64 { 356 | return (curr - start) / uint64(interval.Nanoseconds()) 357 | } 358 | -------------------------------------------------------------------------------- /memorystore/store_test.go: -------------------------------------------------------------------------------- 1 | package memorystore 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "crypto/sha256" 7 | "fmt" 8 | "sort" 9 | "testing" 10 | "time" 11 | 12 | "github.com/sethvargo/go-limiter/internal/fasttime" 13 | ) 14 | 15 | func TestFillRate(t *testing.T) { 16 | t.Parallel() 17 | 18 | t.Run("many_tokens_small_interval", func(t *testing.T) { 19 | t.Parallel() 20 | 21 | s, _ := New(&Config{ 22 | Tokens: 65525, 23 | Interval: time.Second, 24 | }) 25 | 26 | for i := 0; i < 20; i++ { 27 | limit, remaining, _, _, _ := s.Take(context.Background(), "key") 28 | if remaining < limit-uint64(i)-1 { 29 | t.Errorf("invalid remaining: run: %d limit: %d remaining: %d", i, limit, remaining) 30 | } 31 | time.Sleep(100 * time.Millisecond) 32 | } 33 | }) 34 | } 35 | 36 | func testKey(tb testing.TB) string { 37 | tb.Helper() 38 | 39 | var b [512]byte 40 | if _, err := rand.Read(b[:]); err != nil { 41 | tb.Fatalf("failed to generate random string: %v", err) 42 | } 43 | digest := fmt.Sprintf("%x", sha256.Sum256(b[:])) 44 | return digest[:32] 45 | } 46 | 47 | func TestStore_Exercise(t *testing.T) { 48 | t.Parallel() 49 | 50 | ctx := context.Background() 51 | 52 | s, err := New(&Config{ 53 | Tokens: 5, 54 | Interval: 3 * time.Second, 55 | SweepInterval: 24 * time.Hour, 56 | SweepMinTTL: 24 * time.Hour, 57 | }) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | t.Cleanup(func() { 62 | if err := s.Close(ctx); err != nil { 63 | t.Fatal(err) 64 | } 65 | }) 66 | 67 | key := testKey(t) 68 | 69 | // Get when no config exists 70 | { 71 | limit, remaining, err := s.(*store).Get(ctx, key) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | 76 | if got, want := limit, uint64(0); got != want { 77 | t.Errorf("expected %v to be %v", got, want) 78 | } 79 | if got, want := remaining, uint64(0); got != want { 80 | t.Errorf("expected %v to be %v", got, want) 81 | } 82 | } 83 | 84 | // Take with no key configuration - this should use the default values 85 | { 86 | limit, remaining, reset, ok, err := s.Take(ctx, key) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | if !ok { 91 | t.Errorf("expected ok") 92 | } 93 | if got, want := limit, uint64(5); got != want { 94 | t.Errorf("expected %v to be %v", got, want) 95 | } 96 | if got, want := remaining, uint64(4); got != want { 97 | t.Errorf("expected %v to be %v", got, want) 98 | } 99 | if got, want := time.Until(time.Unix(0, int64(reset))), 3*time.Second; got > want { 100 | t.Errorf("expected %v to less than %v", got, want) 101 | } 102 | } 103 | 104 | // Get the value 105 | { 106 | limit, remaining, err := s.(*store).Get(ctx, key) 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | if got, want := limit, uint64(5); got != want { 111 | t.Errorf("expected %v to be %v", got, want) 112 | } 113 | if got, want := remaining, uint64(4); got != want { 114 | t.Errorf("expected %v to be %v", got, want) 115 | } 116 | } 117 | 118 | // Now set a value 119 | { 120 | if err := s.Set(ctx, key, 11, 5*time.Second); err != nil { 121 | t.Fatal(err) 122 | } 123 | } 124 | 125 | // Get the value again 126 | { 127 | limit, remaining, err := s.(*store).Get(ctx, key) 128 | if err != nil { 129 | t.Fatal(err) 130 | } 131 | if got, want := limit, uint64(11); got != want { 132 | t.Errorf("expected %v to be %v", got, want) 133 | } 134 | if got, want := remaining, uint64(11); got != want { 135 | t.Errorf("expected %v to be %v", got, want) 136 | } 137 | } 138 | 139 | // Take again, this should use the new values 140 | { 141 | limit, remaining, reset, ok, err := s.Take(ctx, key) 142 | if err != nil { 143 | t.Fatal(err) 144 | } 145 | if !ok { 146 | t.Errorf("expected ok") 147 | } 148 | if got, want := limit, uint64(11); got != want { 149 | t.Errorf("expected %v to be %v", got, want) 150 | } 151 | if got, want := remaining, uint64(10); got != want { 152 | t.Errorf("expected %v to be %v", got, want) 153 | } 154 | if got, want := time.Until(time.Unix(0, int64(reset))), 5*time.Second; got > want { 155 | t.Errorf("expected %v to less than %v", got, want) 156 | } 157 | } 158 | 159 | // Get the value again 160 | { 161 | limit, remaining, err := s.(*store).Get(ctx, key) 162 | if err != nil { 163 | t.Fatal(err) 164 | } 165 | if got, want := limit, uint64(11); got != want { 166 | t.Errorf("expected %v to be %v", got, want) 167 | } 168 | if got, want := remaining, uint64(10); got != want { 169 | t.Errorf("expected %v to be %v", got, want) 170 | } 171 | } 172 | 173 | // Burst and take 174 | { 175 | if err := s.Burst(ctx, key, 5); err != nil { 176 | t.Fatal(err) 177 | } 178 | 179 | limit, remaining, reset, ok, err := s.Take(ctx, key) 180 | if err != nil { 181 | t.Fatal(err) 182 | } 183 | if !ok { 184 | t.Errorf("expected ok") 185 | } 186 | if got, want := limit, uint64(11); got != want { 187 | t.Errorf("expected %v to be %v", got, want) 188 | } 189 | if got, want := remaining, uint64(14); got != want { 190 | t.Errorf("expected %v to be %v", got, want) 191 | } 192 | if got, want := time.Until(time.Unix(0, int64(reset))), 5*time.Second; got > want { 193 | t.Errorf("expected %v to less than %v", got, want) 194 | } 195 | } 196 | 197 | // Get the value one final time 198 | { 199 | limit, remaining, err := s.(*store).Get(ctx, key) 200 | if err != nil { 201 | t.Fatal(err) 202 | } 203 | if got, want := limit, uint64(11); got != want { 204 | t.Errorf("expected %v to be %v", got, want) 205 | } 206 | if got, want := remaining, uint64(14); got != want { 207 | t.Errorf("expected %v to be %v", got, want) 208 | } 209 | } 210 | } 211 | 212 | func TestStore_Take(t *testing.T) { 213 | t.Parallel() 214 | 215 | ctx := context.Background() 216 | 217 | cases := []struct { 218 | name string 219 | tokens uint64 220 | interval time.Duration 221 | }{ 222 | { 223 | name: "milli", 224 | tokens: 5, 225 | interval: 500 * time.Millisecond, 226 | }, 227 | { 228 | name: "second", 229 | tokens: 10, 230 | interval: 1 * time.Second, 231 | }, 232 | } 233 | 234 | for _, tc := range cases { 235 | tc := tc 236 | 237 | t.Run(tc.name, func(t *testing.T) { 238 | t.Parallel() 239 | 240 | key := testKey(t) 241 | 242 | s, err := New(&Config{ 243 | Interval: tc.interval, 244 | Tokens: tc.tokens, 245 | SweepInterval: 24 * time.Hour, 246 | SweepMinTTL: 24 * time.Hour, 247 | }) 248 | if err != nil { 249 | t.Fatal(err) 250 | } 251 | t.Cleanup(func() { 252 | if err := s.Close(ctx); err != nil { 253 | t.Fatal(err) 254 | } 255 | }) 256 | 257 | type result struct { 258 | limit, remaining uint64 259 | reset time.Duration 260 | ok bool 261 | err error 262 | } 263 | 264 | // Take twice everything from the bucket. 265 | takeCh := make(chan *result, 2*tc.tokens) 266 | for i := uint64(1); i <= 2*tc.tokens; i++ { 267 | go func() { 268 | limit, remaining, reset, ok, err := s.Take(ctx, key) 269 | takeCh <- &result{limit, remaining, time.Duration(fasttime.Now() - reset), ok, err} 270 | }() 271 | } 272 | 273 | // Accumulate and sort results, since they could come in any order. 274 | var results []*result 275 | for i := uint64(1); i <= 2*tc.tokens; i++ { 276 | select { 277 | case result := <-takeCh: 278 | results = append(results, result) 279 | case <-time.After(5 * time.Second): 280 | t.Fatal("timeout") 281 | } 282 | } 283 | sort.Slice(results, func(i, j int) bool { 284 | if results[i].remaining == results[j].remaining { 285 | return !results[j].ok 286 | } 287 | return results[i].remaining > results[j].remaining 288 | }) 289 | 290 | for i, result := range results { 291 | if err := result.err; err != nil { 292 | t.Fatal(err) 293 | } 294 | 295 | if got, want := result.limit, tc.tokens; got != want { 296 | t.Errorf("limit: expected %d to be %d", got, want) 297 | } 298 | if got, want := result.reset, tc.interval; got > want { 299 | t.Errorf("reset: expected %d to be less than %d", got, want) 300 | } 301 | 302 | // first half should pass, second half should fail 303 | if uint64(i) < tc.tokens { 304 | if got, want := result.remaining, tc.tokens-uint64(i)-1; got != want { 305 | t.Errorf("remaining: expected %d to be %d", got, want) 306 | } 307 | if got, want := result.ok, true; got != want { 308 | t.Errorf("ok: expected %t to be %t", got, want) 309 | } 310 | } else { 311 | if got, want := result.remaining, uint64(0); got != want { 312 | t.Errorf("remaining: expected %d to be %d", got, want) 313 | } 314 | if got, want := result.ok, false; got != want { 315 | t.Errorf("ok: expected %t to be %t", got, want) 316 | } 317 | } 318 | } 319 | 320 | // Wait for the bucket to have entries again. 321 | time.Sleep(tc.interval) 322 | 323 | // Verify we can take once more. 324 | _, _, _, ok, err := s.Take(ctx, key) 325 | if err != nil { 326 | t.Fatal(err) 327 | } 328 | if !ok { 329 | t.Errorf("expected %t to be %t", ok, true) 330 | } 331 | }) 332 | } 333 | } 334 | 335 | func TestBucketedLimiter_tick(t *testing.T) { 336 | t.Parallel() 337 | 338 | cases := []struct { 339 | name string 340 | start uint64 341 | curr uint64 342 | interval time.Duration 343 | exp uint64 344 | }{ 345 | { 346 | name: "no_diff", 347 | start: 0, 348 | curr: 0, 349 | interval: time.Second, 350 | exp: 0, 351 | }, 352 | { 353 | name: "half", 354 | start: 0, 355 | curr: uint64(500 * time.Millisecond), 356 | interval: time.Second, 357 | exp: 0, 358 | }, 359 | { 360 | name: "almost", 361 | start: 0, 362 | curr: uint64(1*time.Second - time.Nanosecond), 363 | interval: time.Second, 364 | exp: 0, 365 | }, 366 | { 367 | name: "exact", 368 | start: 0, 369 | curr: uint64(1 * time.Second), 370 | interval: time.Second, 371 | exp: 1, 372 | }, 373 | { 374 | name: "multiple", 375 | start: 0, 376 | curr: uint64(50*time.Second - 500*time.Millisecond), 377 | interval: time.Second, 378 | exp: 49, 379 | }, 380 | { 381 | name: "short", 382 | start: 0, 383 | curr: uint64(50*time.Second - 500*time.Millisecond), 384 | interval: time.Millisecond, 385 | exp: 49500, 386 | }, 387 | } 388 | 389 | for _, tc := range cases { 390 | tc := tc 391 | 392 | t.Run(tc.name, func(t *testing.T) { 393 | t.Parallel() 394 | 395 | if got, want := tick(tc.start, tc.curr, tc.interval), tc.exp; got != want { 396 | t.Errorf("expected %v to be %v", got, want) 397 | } 398 | }) 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /noopstore/example_test.go: -------------------------------------------------------------------------------- 1 | package noopstore_test 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/sethvargo/go-limiter/noopstore" 8 | ) 9 | 10 | func ExampleNew() { 11 | ctx := context.Background() 12 | 13 | store, err := noopstore.New() 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | defer store.Close(ctx) 18 | 19 | limit, remaining, reset, ok, err := store.Take(ctx, "my-key") 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | _, _, _, _ = limit, remaining, reset, ok 24 | } 25 | -------------------------------------------------------------------------------- /noopstore/store.go: -------------------------------------------------------------------------------- 1 | // Package noopstore defines a storage system for limiting that always allows 2 | // requests. It's an empty store useful for testing or development. 3 | package noopstore 4 | 5 | import ( 6 | "context" 7 | "time" 8 | 9 | "github.com/sethvargo/go-limiter" 10 | ) 11 | 12 | var _ limiter.Store = (*store)(nil) 13 | 14 | type store struct{} 15 | 16 | func New() (limiter.Store, error) { 17 | return &store{}, nil 18 | } 19 | 20 | // Take always allows the request. 21 | func (s *store) Take(_ context.Context, _ string) (uint64, uint64, uint64, bool, error) { 22 | return 0, 0, 0, true, nil 23 | } 24 | 25 | // Get does nothing. 26 | func (s *store) Get(_ context.Context, _ string) (uint64, uint64, error) { 27 | return 0, 0, nil 28 | } 29 | 30 | // Set does nothing. 31 | func (s *store) Set(_ context.Context, _ string, _ uint64, _ time.Duration) error { 32 | return nil 33 | } 34 | 35 | // Burst does nothing. 36 | func (s *store) Burst(_ context.Context, _ string, _ uint64) error { 37 | return nil 38 | } 39 | 40 | // Close does nothing. 41 | func (s *store) Close(_ context.Context) error { 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | // ErrStopped is the error returned when the store is stopped. All implementers 10 | // should return this error for stoppable stores. 11 | var ErrStopped = fmt.Errorf("store is stopped") 12 | 13 | // Store is an interface for limiter storage backends. 14 | // 15 | // Keys should be hash, sanitized, or otherwise scrubbed of identifiable 16 | // information they will be given to the store in plaintext. If you're rate 17 | // limiting by IP address, for example, the IP address would be stored in the 18 | // storage system in plaintext. This may be undesirable in certain situations, 19 | // like when the store is a public database. In those cases, you should hash or 20 | // HMAC the key before passing giving it to the store. If you want to encrypt 21 | // the value, you must use homomorphic encryption to ensure the value always 22 | // encrypts to the same ciphertext. 23 | type Store interface { 24 | // Take takes a token from the given key if available, returning: 25 | // 26 | // - the configured limit size 27 | // - the number of remaining tokens in the interval 28 | // - the server time when new tokens will be available 29 | // - whether the take was successful 30 | // - any errors that occurred while performing the take - these should be 31 | // backend errors (e.g. connection failures); Take() should never return an 32 | // error for an bucket. 33 | // 34 | // If "ok" is false, the take was unsuccessful and the caller should NOT 35 | // service the request. 36 | // 37 | // See the note about keys on the interface documentation. 38 | Take(ctx context.Context, key string) (tokens, remaining, reset uint64, ok bool, err error) 39 | 40 | // Get gets the current limit and remaining tokens for the provided key. It 41 | // does not change any of the values. 42 | Get(ctx context.Context, key string) (tokens, remaining uint64, err error) 43 | 44 | // Set configures the limit at the provided key. If a limit already exists, it 45 | // is overwritten. This also sets the number of tokens in the bucket to the 46 | // limit. 47 | Set(ctx context.Context, key string, tokens uint64, interval time.Duration) error 48 | 49 | // Burst adds more tokens to the key's current bucket until the next interval 50 | // tick. This will allow the current bucket tick to exceed the maximum number 51 | // maximum ticks until the next interval. 52 | Burst(ctx context.Context, key string, tokens uint64) error 53 | 54 | // Close terminates the store and cleans up any data structures or connections 55 | // that may remain open. After a store is stopped, Take() should always return 56 | // zero values. 57 | Close(ctx context.Context) error 58 | } 59 | -------------------------------------------------------------------------------- /tools/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sethvargo/go-limiter/tools 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/client9/misspell v0.3.4 7 | golang.org/x/tools v0.0.0-20200702044944-0cc1aa72b347 8 | honnef.co/go/tools v0.0.1-2020.1.4 9 | ) 10 | -------------------------------------------------------------------------------- /tools/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= 4 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 5 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 6 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 7 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 8 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 9 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 10 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 11 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 12 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 13 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 14 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 15 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 16 | golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= 17 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 18 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 19 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 20 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 21 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 22 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 23 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 24 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 26 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 27 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 28 | golang.org/x/tools v0.0.0-20200702044944-0cc1aa72b347 h1:/e4fNMHdLn7SQSxTrRZTma2xjQW6ELdxcnpqMhpo9X4= 29 | golang.org/x/tools v0.0.0-20200702044944-0cc1aa72b347/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 30 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 31 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 32 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 33 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 34 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 35 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 36 | honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= 37 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 38 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // Package tools includes the list of tools used in the project. 2 | package tools 3 | 4 | import ( 5 | _ "github.com/client9/misspell/cmd/misspell" 6 | _ "golang.org/x/tools/cmd/goimports" 7 | _ "honnef.co/go/tools/cmd/staticcheck" 8 | ) 9 | --------------------------------------------------------------------------------