├── .github └── workflows │ └── ci.yml ├── LICENSE ├── README.md ├── _example ├── go.mod ├── go.sum └── main.go ├── config.go ├── go.mod ├── go.sum ├── httprateredis.go ├── httprateredis_test.go └── local_fallback_test.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | tests: 9 | name: Tests 10 | runs-on: ubuntu-latest 11 | 12 | services: 13 | redis: 14 | image: redis:7 15 | ports: 16 | - 6379:6379 17 | 18 | steps: 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: ^1.17 23 | 24 | - name: Check out code into the Go module directory 25 | uses: actions/checkout@v4 26 | 27 | - name: Get dependencies 28 | run: go get -v -t -d ./... 29 | 30 | - name: Build 31 | run: go build -v ./ 32 | 33 | - name: Build example 34 | run: cd ./_example && go build -v ./ 35 | 36 | - name: Check ulimit 37 | run: ulimit -n 38 | 39 | - name: Test 40 | run: go test -v -count=10 ./... 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 go-chi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # httprate-redis 2 | 3 | ![CI workflow](https://github.com/go-chi/httprate-redis/actions/workflows/ci.yml/badge.svg) 4 | [![GoDoc Widget]][GoDoc] 5 | 6 | [GoDoc]: https://pkg.go.dev/github.com/go-chi/httprate-redis 7 | [GoDoc Widget]: https://godoc.org/github.com/go-chi/httprate-redis?status.svg 8 | 9 | Redis backend for [github.com/go-chi/httprate](https://github.com/go-chi/httprate), implementing `httprate.LimitCounter` interface. 10 | 11 | See [_example/main.go](./_example/main.go) for usage. 12 | 13 | ## Example 14 | 15 | ```go 16 | package main 17 | 18 | import ( 19 | "net/http" 20 | 21 | "github.com/go-chi/chi/v5" 22 | "github.com/go-chi/chi/v5/middleware" 23 | "github.com/go-chi/httprate" 24 | httprateredis "github.com/go-chi/httprate-redis" 25 | ) 26 | 27 | func main() { 28 | r := chi.NewRouter() 29 | r.Use(middleware.Logger) 30 | 31 | r.Use(httprate.Limit( 32 | 5, 33 | time.Minute, 34 | httprate.WithKeyByIP(), 35 | httprateredis.WithRedisLimitCounter(&httprateredis.Config{ 36 | Host: "127.0.0.1", Port: 6379, 37 | }), 38 | )) 39 | 40 | r.Get("/", func(w http.ResponseWriter, r *http.Request) { 41 | w.Write([]byte("This is IP rate-limited by 5 req/min")) 42 | }) 43 | 44 | http.ListenAndServe(":3333", r) 45 | } 46 | ``` 47 | 48 | ## LICENSE 49 | 50 | MIT 51 | -------------------------------------------------------------------------------- /_example/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-chi/httprate-redis/_example 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | replace github.com/go-chi/httprate-redis => ../ 8 | 9 | require ( 10 | github.com/go-chi/chi/v5 v5.1.0 11 | github.com/go-chi/httprate v0.15.0 12 | github.com/go-chi/httprate-redis v0.3.0 13 | github.com/go-chi/telemetry v0.3.4 14 | ) 15 | 16 | require ( 17 | github.com/beorn7/perks v1.0.1 // indirect 18 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 19 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 20 | github.com/golang/mock v1.6.0 // indirect 21 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 22 | github.com/pkg/errors v0.9.1 // indirect 23 | github.com/prometheus/client_golang v1.19.0 // indirect 24 | github.com/prometheus/client_model v0.6.1 // indirect 25 | github.com/prometheus/common v0.52.3 // indirect 26 | github.com/prometheus/procfs v0.13.0 // indirect 27 | github.com/redis/go-redis/v9 v9.7.3 // indirect 28 | github.com/twmb/murmur3 v1.1.8 // indirect 29 | github.com/uber-go/tally/v4 v4.1.16 // indirect 30 | github.com/zeebo/xxh3 v1.0.2 // indirect 31 | go.uber.org/atomic v1.11.0 // indirect 32 | golang.org/x/sys v0.30.0 // indirect 33 | google.golang.org/protobuf v1.33.0 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /_example/go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 4 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 5 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 6 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 7 | github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= 8 | github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= 9 | github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0= 10 | github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqRWqM3nksiyXk5s8= 11 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 12 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 13 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 14 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 15 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 16 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 17 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 18 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 19 | github.com/cactus/go-statsd-client/v5 v5.0.0/go.mod h1:COEvJ1E+/E2L4q6QE5CkjWPi4eeDw9maJBMIuMPBZbY= 20 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 21 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 22 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 27 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 28 | github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= 29 | github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 30 | github.com/go-chi/httprate v0.15.0 h1:j54xcWV9KGmPf/X4H32/aTH+wBlrvxL7P+SdnRqxh5g= 31 | github.com/go-chi/httprate v0.15.0/go.mod h1:rzGHhVrsBn3IMLYDOZQsSU4fJNWcjui4fWKJcCId1R4= 32 | github.com/go-chi/telemetry v0.3.4 h1:iCe1lbqP4pOOYkxyy3Y2LjkNA1iMgp1owp0JYgdTRhM= 33 | github.com/go-chi/telemetry v0.3.4/go.mod h1:N+qwgqriyLwEPFyXAjj22GMdDlNuCaEourUPJfZBoPw= 34 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 35 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 36 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 37 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 38 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 39 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 40 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 41 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 42 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 43 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 44 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 45 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 46 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 47 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 48 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 49 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 50 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 51 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 52 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 53 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 54 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 55 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 56 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 57 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 58 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 59 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 60 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 61 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 62 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 63 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 64 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 65 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 66 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 67 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 68 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 69 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 70 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 71 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 72 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 73 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 74 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 75 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 76 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 77 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 78 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 79 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 80 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 81 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 82 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 83 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 84 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 85 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 86 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 87 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 88 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 89 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 90 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 91 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 92 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 93 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 94 | github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= 95 | github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= 96 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 97 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 98 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 99 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 100 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 101 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 102 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 103 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 104 | github.com/prometheus/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA= 105 | github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= 106 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 107 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 108 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 109 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 110 | github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= 111 | github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= 112 | github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 113 | github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= 114 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 115 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 116 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 117 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 118 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 119 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 120 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 121 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 122 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 123 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 124 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 125 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 126 | github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= 127 | github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= 128 | github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= 129 | github.com/uber-go/tally/v4 v4.1.16 h1:by2hveWRh/cUReButk6ns1sHK/hiKry7BuOV6iY16XI= 130 | github.com/uber-go/tally/v4 v4.1.16/go.mod h1:RW5DgqsyEPs0lA4b0YNf4zKj7DveKHd73hnO6zVlyW0= 131 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 132 | github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= 133 | github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= 134 | github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= 135 | github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= 136 | github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= 137 | github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= 138 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 139 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 140 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 141 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 142 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 143 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 144 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 145 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 146 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 147 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 148 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 149 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 150 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 151 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 152 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 153 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 154 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 155 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 156 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 157 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 158 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 159 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 160 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 161 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 162 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 163 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 164 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 165 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 166 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 167 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 168 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 169 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 170 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 171 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 172 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 173 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 174 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 175 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 176 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 177 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 178 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 179 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 180 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 181 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 182 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 183 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 184 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 185 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 186 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 187 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 188 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 189 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 190 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 191 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 192 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 193 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 194 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 195 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 196 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 197 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 198 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 199 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 200 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 201 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 202 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 203 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 204 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 205 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 206 | gopkg.in/validator.v2 v2.0.0-20200605151824-2b28d334fa05/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= 207 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 208 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 209 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 210 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 211 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 212 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 213 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 214 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 215 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 216 | -------------------------------------------------------------------------------- /_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/go-chi/chi/v5" 9 | "github.com/go-chi/chi/v5/middleware" 10 | "github.com/go-chi/httprate" 11 | httprateredis "github.com/go-chi/httprate-redis" 12 | "github.com/go-chi/telemetry" 13 | ) 14 | 15 | func main() { 16 | r := chi.NewRouter() 17 | r.Use(middleware.Logger) 18 | 19 | // Expose Prometheus endpoint at /metrics path. 20 | r.Use(telemetry.Collector(telemetry.Config{AllowAny: true})) 21 | 22 | rc := httprateredis.NewCounter(&httprateredis.Config{ 23 | Host: "127.0.0.1", Port: 6379, 24 | }) 25 | 26 | r.Group(func(r chi.Router) { 27 | // Set an extra header demonstrating which backend is currently 28 | // in use (redis vs. local in-memory fallback). 29 | r.Use(func(next http.Handler) http.Handler { 30 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 31 | if rc.IsFallbackActivated() { 32 | w.Header().Set("X-RateLimit-Backend", "in-memory") 33 | } else { 34 | w.Header().Set("X-RateLimit-Backend", "redis") 35 | } 36 | next.ServeHTTP(w, r) 37 | }) 38 | }) 39 | 40 | // Rate-limit at 50 req/s per IP address. 41 | r.Use(httprate.Limit( 42 | 50, time.Second, 43 | httprate.WithKeyByIP(), 44 | httprateredis.WithRedisLimitCounter(&httprateredis.Config{ 45 | Host: "127.0.0.1", Port: 6379, 46 | }), 47 | httprate.WithLimitCounter(rc), 48 | )) 49 | 50 | r.Get("/", func(w http.ResponseWriter, r *http.Request) { 51 | w.Write([]byte("ok\n")) 52 | }) 53 | }) 54 | 55 | log.Printf("Serving at http://localhost:3333, rate-limited at 50 req/s per IP address") 56 | log.Println() 57 | log.Printf("Try making 55 requests:") 58 | log.Println(`curl -s -o /dev/null -w "Request #%{xfer_id} => Response HTTP %{http_code} (backend: %header{X-Ratelimit-Backend}, limit: %header{X-Ratelimit-Limit}, remaining: %header{X-Ratelimit-Remaining})\n" "http://localhost:3333?req=[0-54]"`) 59 | 60 | http.ListenAndServe(":3333", r) 61 | } 62 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package httprateredis 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/redis/go-redis/v9" 7 | ) 8 | 9 | type Config struct { 10 | Disabled bool `toml:"disabled"` // default: false 11 | 12 | WindowLength time.Duration `toml:"window_length"` // default: 1m 13 | ClientName string `toml:"client_name"` // default: "" 14 | PrefixKey string `toml:"prefix_key"` // default: "httprate" 15 | 16 | // OnError lets you subscribe to all runtime Redis errors. Useful for logging/debugging. 17 | OnError func(err error) 18 | 19 | // Disable the use of the local in-memory fallback mechanism. When enabled, 20 | // the system will return HTTP 428 for all requests when Redis is down. 21 | FallbackDisabled bool `toml:"fallback_disabled"` // default: false 22 | 23 | // Timeout for each Redis command after which we fall back to a local 24 | // in-memory counter. If Redis does not respond within this duration, 25 | // the system will use the local counter unless it is explicitly disabled. 26 | FallbackTimeout time.Duration `toml:"fallback_timeout"` // default: 100ms 27 | 28 | // OnFallbackChange lets subscribe to local in-memory fallback changes. 29 | OnFallbackChange func(activated bool) 30 | 31 | // Client if supplied will be used and the below fields will be ignored. 32 | // 33 | // NOTE: It's recommended to set short dial/read/write timeouts and disable 34 | // retries on the client, so the local in-memory fallback can activate quickly. 35 | Client *redis.Client `toml:"-"` 36 | Host string `toml:"host"` 37 | Port uint16 `toml:"port"` 38 | Password string `toml:"password"` // optional 39 | DBIndex int `toml:"db_index"` // default: 0 40 | MaxIdle int `toml:"max_idle"` // default: 5 41 | MaxActive int `toml:"max_active"` // default: 10 42 | } 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-chi/httprate-redis 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/alicebob/miniredis/v2 v2.34.0 9 | github.com/go-chi/httprate v0.15.0 10 | github.com/redis/go-redis/v9 v9.7.3 11 | golang.org/x/sync v0.12.0 12 | ) 13 | 14 | require ( 15 | github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect 16 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 17 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 18 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 19 | github.com/yuin/gopher-lua v1.1.1 // indirect 20 | github.com/zeebo/xxh3 v1.0.2 // indirect 21 | golang.org/x/sys v0.30.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= 2 | github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= 3 | github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0= 4 | github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqRWqM3nksiyXk5s8= 5 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 6 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 7 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 8 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 9 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 10 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 12 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 13 | github.com/go-chi/httprate v0.15.0 h1:j54xcWV9KGmPf/X4H32/aTH+wBlrvxL7P+SdnRqxh5g= 14 | github.com/go-chi/httprate v0.15.0/go.mod h1:rzGHhVrsBn3IMLYDOZQsSU4fJNWcjui4fWKJcCId1R4= 15 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 16 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 17 | github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 18 | github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= 19 | github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= 20 | github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= 21 | github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= 22 | github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= 23 | github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= 24 | github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= 25 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 26 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 27 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 28 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 29 | -------------------------------------------------------------------------------- /httprateredis.go: -------------------------------------------------------------------------------- 1 | package httprateredis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/go-chi/httprate" 11 | "github.com/redis/go-redis/v9" 12 | ) 13 | 14 | func WithRedisLimitCounter(cfg *Config) httprate.Option { 15 | if cfg.Disabled { 16 | return httprate.WithNoop() 17 | } 18 | return httprate.WithLimitCounter(NewCounter(cfg)) 19 | } 20 | 21 | func NewRedisLimitCounter(cfg *Config) (*redisCounter, error) { 22 | c := NewCounter(cfg) 23 | if err := c.client.Ping(context.Background()).Err(); err != nil { 24 | return nil, fmt.Errorf("ping failed: %w", err) 25 | } 26 | return c, nil 27 | } 28 | 29 | func NewCounter(cfg *Config) *redisCounter { 30 | if cfg == nil { 31 | cfg = &Config{} 32 | } 33 | if cfg.Host == "" { 34 | cfg.Host = "127.0.0.1" 35 | } 36 | if cfg.Port < 1 { 37 | cfg.Port = 6379 38 | } 39 | if cfg.PrefixKey == "" { 40 | cfg.PrefixKey = "httprate" 41 | } 42 | if cfg.FallbackTimeout == 0 { 43 | if cfg.FallbackDisabled { 44 | cfg.FallbackTimeout = time.Second 45 | } else { 46 | // Activate local in-memory fallback fairly quickly, 47 | // so we don't slow down incoming requests too much. 48 | cfg.FallbackTimeout = 250 * time.Millisecond 49 | } 50 | } 51 | 52 | rc := &redisCounter{ 53 | prefixKey: cfg.PrefixKey, 54 | onError: func(err error) {}, 55 | onFallback: func(activated bool) {}, 56 | } 57 | if cfg.OnError != nil { 58 | rc.onError = cfg.OnError 59 | } 60 | if !cfg.FallbackDisabled { 61 | rc.fallbackCounter = httprate.NewLocalLimitCounter(cfg.WindowLength) 62 | if cfg.OnFallbackChange != nil { 63 | rc.onFallback = cfg.OnFallbackChange 64 | } 65 | } 66 | 67 | if cfg.Client != nil { 68 | rc.client = cfg.Client 69 | } else { 70 | maxIdle, maxActive := cfg.MaxIdle, cfg.MaxActive 71 | if maxIdle < 1 { 72 | maxIdle = 5 73 | } 74 | if maxActive < 1 { 75 | maxActive = 10 76 | } 77 | 78 | rc.client = redis.NewClient(&redis.Options{ 79 | Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port), 80 | Password: cfg.Password, 81 | DB: cfg.DBIndex, 82 | ClientName: cfg.ClientName, 83 | DisableIndentity: true, 84 | 85 | DialTimeout: 2 * cfg.FallbackTimeout, 86 | ReadTimeout: cfg.FallbackTimeout, 87 | WriteTimeout: cfg.FallbackTimeout, 88 | PoolSize: maxActive, 89 | MinIdleConns: 1, 90 | MaxIdleConns: maxIdle, 91 | MaxRetries: -1, // -1 disables retries 92 | }) 93 | } 94 | 95 | return rc 96 | } 97 | 98 | type redisCounter struct { 99 | client *redis.Client 100 | windowLength time.Duration 101 | prefixKey string 102 | fallbackActivated atomic.Bool 103 | fallbackCounter httprate.LimitCounter 104 | onError func(err error) 105 | onFallback func(activated bool) 106 | } 107 | 108 | var _ httprate.LimitCounter = (*redisCounter)(nil) 109 | 110 | func (c *redisCounter) Config(requestLimit int, windowLength time.Duration) { 111 | c.windowLength = windowLength 112 | if c.fallbackCounter != nil { 113 | c.fallbackCounter.Config(requestLimit, windowLength) 114 | } 115 | } 116 | 117 | func (c *redisCounter) Increment(key string, currentWindow time.Time) error { 118 | return c.IncrementBy(key, currentWindow, 1) 119 | } 120 | 121 | func (c *redisCounter) IncrementBy(key string, currentWindow time.Time, amount int) (err error) { 122 | if c.fallbackCounter != nil { 123 | if c.fallbackActivated.Load() { 124 | return c.fallbackCounter.IncrementBy(key, currentWindow, amount) 125 | } 126 | defer func() { 127 | if c.shouldFallback(err) { 128 | err = c.fallbackCounter.IncrementBy(key, currentWindow, amount) 129 | } 130 | }() 131 | } 132 | 133 | // Note: Timeouts are set up directly on the Redis client. 134 | ctx := context.Background() 135 | 136 | hkey := c.limitCounterKey(key, currentWindow) 137 | 138 | pipe := c.client.TxPipeline() 139 | incrCmd := pipe.IncrBy(ctx, hkey, int64(amount)) 140 | expireCmd := pipe.Expire(ctx, hkey, c.windowLength*3) 141 | 142 | _, err = pipe.Exec(ctx) 143 | if err != nil { 144 | return fmt.Errorf("httprateredis: redis transaction failed: %w", err) 145 | } 146 | if err := incrCmd.Err(); err != nil { 147 | return fmt.Errorf("httprateredis: redis incr failed: %w", err) 148 | } 149 | if err := expireCmd.Err(); err != nil { 150 | return fmt.Errorf("httprateredis: redis expire failed: %w", err) 151 | } 152 | 153 | return nil 154 | } 155 | 156 | func (c *redisCounter) Get(key string, currentWindow, previousWindow time.Time) (curr int, prev int, err error) { 157 | if c.fallbackCounter != nil { 158 | if c.fallbackActivated.Load() { 159 | return c.fallbackCounter.Get(key, currentWindow, previousWindow) 160 | } 161 | defer func() { 162 | if c.shouldFallback(err) { 163 | curr, prev, err = c.fallbackCounter.Get(key, currentWindow, previousWindow) 164 | } 165 | }() 166 | } 167 | 168 | // Note: Timeouts are set up directly on the Redis client. 169 | ctx := context.Background() 170 | 171 | currKey := c.limitCounterKey(key, currentWindow) 172 | prevKey := c.limitCounterKey(key, previousWindow) 173 | 174 | values, err := c.client.MGet(ctx, currKey, prevKey).Result() 175 | if err != nil { 176 | return 0, 0, fmt.Errorf("httprateredis: redis mget failed: %w", err) 177 | } else if len(values) != 2 { 178 | return 0, 0, fmt.Errorf("httprateredis: redis mget returned wrong number of keys: %v, expected 2", len(values)) 179 | } 180 | 181 | // MGET always returns slice with nil or "string" values, even if the values 182 | // were created with the INCR command. Ignore error if we can't parse the number. 183 | if values[0] != nil { 184 | v, _ := values[0].(string) 185 | curr, _ = strconv.Atoi(v) 186 | } 187 | if values[1] != nil { 188 | v, _ := values[1].(string) 189 | prev, _ = strconv.Atoi(v) 190 | } 191 | 192 | return curr, prev, nil 193 | } 194 | 195 | func (c *redisCounter) IsFallbackActivated() bool { 196 | return c.fallbackActivated.Load() 197 | } 198 | 199 | func (c *redisCounter) Close() error { 200 | return c.client.Close() 201 | } 202 | 203 | func (c *redisCounter) shouldFallback(err error) bool { 204 | if err == nil { 205 | return false 206 | } 207 | c.onError(err) 208 | 209 | // Activate the local in-memory counter fallback, unless activated by some other goroutine. 210 | alreadyActivated := c.fallbackActivated.Swap(true) 211 | if !alreadyActivated { 212 | c.onFallback(true) 213 | go c.reconnect() 214 | } 215 | 216 | return true 217 | } 218 | 219 | func (c *redisCounter) reconnect() { 220 | // Try to re-connect to redis every 200ms. 221 | for { 222 | time.Sleep(200 * time.Millisecond) 223 | 224 | err := c.client.Ping(context.Background()).Err() 225 | if err == nil { 226 | c.fallbackActivated.Store(false) 227 | if c.onFallback != nil { 228 | c.onFallback(false) 229 | } 230 | return 231 | } 232 | } 233 | } 234 | 235 | func (c *redisCounter) limitCounterKey(key string, window time.Time) string { 236 | return fmt.Sprintf("%s:%d", c.prefixKey, httprate.LimitCounterKey(key, window)) 237 | } 238 | -------------------------------------------------------------------------------- /httprateredis_test.go: -------------------------------------------------------------------------------- 1 | package httprateredis_test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | httprateredis "github.com/go-chi/httprate-redis" 11 | "golang.org/x/sync/errgroup" 12 | ) 13 | 14 | func TestRedisCounter(t *testing.T) { 15 | limitCounter := httprateredis.NewCounter(&httprateredis.Config{ 16 | Host: "localhost", 17 | Port: 6379, 18 | MaxIdle: 0, 19 | MaxActive: 2, 20 | DBIndex: 0, 21 | ClientName: "httprateredis_test", 22 | PrefixKey: fmt.Sprintf("httprate:test:%v", rand.Int31n(100000)), // Unique Redis key for each test 23 | FallbackTimeout: time.Second, 24 | FallbackDisabled: true, 25 | }) 26 | defer limitCounter.Close() 27 | 28 | limitCounter.Config(1000, time.Minute) 29 | 30 | currentWindow := time.Now().UTC().Truncate(time.Minute) 31 | previousWindow := currentWindow.Add(-time.Minute) 32 | 33 | type test struct { 34 | name string // In each test do the following: 35 | advanceTime time.Duration // 1. advance time 36 | incrBy int // 2. increase counter 37 | prev int // 3. check previous window counter 38 | curr int // and current window counter 39 | } 40 | 41 | tests := []test{ 42 | { 43 | name: "t=0m: init", 44 | prev: 0, 45 | curr: 0, 46 | }, 47 | { 48 | name: "t=0m: increment by 1", 49 | incrBy: 1, 50 | prev: 0, 51 | curr: 1, 52 | }, 53 | { 54 | name: "t=0m: increment by 99", 55 | incrBy: 99, 56 | prev: 0, 57 | curr: 100, 58 | }, 59 | { 60 | name: "t=1m: move clock by 1m", 61 | advanceTime: time.Minute, 62 | prev: 100, 63 | curr: 0, 64 | }, 65 | { 66 | name: "t=1m: increment by 20", 67 | incrBy: 20, 68 | prev: 100, 69 | curr: 20, 70 | }, 71 | { 72 | name: "t=1m: increment by 20", 73 | incrBy: 20, 74 | prev: 100, 75 | curr: 40, 76 | }, 77 | { 78 | name: "t=2m: move clock by 1m", 79 | advanceTime: time.Minute, 80 | prev: 40, 81 | curr: 0, 82 | }, 83 | { 84 | name: "t=2m: incr++", 85 | incrBy: 1, 86 | prev: 40, 87 | curr: 1, 88 | }, 89 | { 90 | name: "t=2m: incr+=9", 91 | incrBy: 9, 92 | prev: 40, 93 | curr: 10, 94 | }, 95 | { 96 | name: "t=2m: incr+=20", 97 | incrBy: 20, 98 | prev: 40, 99 | curr: 30, 100 | }, 101 | { 102 | name: "t=4m: move clock by 2m", 103 | advanceTime: 2 * time.Minute, 104 | prev: 0, 105 | curr: 0, 106 | }, 107 | } 108 | 109 | concurrentRequests := 1000 110 | 111 | for _, tt := range tests { 112 | if tt.advanceTime > 0 { 113 | currentWindow = currentWindow.Add(tt.advanceTime) 114 | previousWindow = previousWindow.Add(tt.advanceTime) 115 | } 116 | 117 | if tt.incrBy > 0 { 118 | var g errgroup.Group 119 | for i := 0; i < concurrentRequests; i++ { 120 | i := i 121 | g.Go(func() error { 122 | key := fmt.Sprintf("key:%v", i) 123 | return limitCounter.IncrementBy(key, currentWindow, tt.incrBy) 124 | }) 125 | } 126 | if err := g.Wait(); err != nil { 127 | t.Errorf("%s: %v", tt.name, err) 128 | } 129 | } 130 | 131 | var g errgroup.Group 132 | for i := 0; i < concurrentRequests; i++ { 133 | i := i 134 | g.Go(func() error { 135 | key := fmt.Sprintf("key:%v", i) 136 | curr, prev, err := limitCounter.Get(key, currentWindow, previousWindow) 137 | if err != nil { 138 | return fmt.Errorf("%q: %w", key, err) 139 | } 140 | if curr != tt.curr { 141 | return fmt.Errorf("%q: unexpected curr = %v, expected %v", key, curr, tt.curr) 142 | } 143 | if prev != tt.prev { 144 | return fmt.Errorf("%q: unexpected prev = %v, expected %v", key, prev, tt.prev) 145 | } 146 | return nil 147 | }) 148 | } 149 | if err := g.Wait(); err != nil { 150 | t.Errorf("%s: %v", tt.name, err) 151 | } 152 | } 153 | } 154 | 155 | func BenchmarkLocalCounter(b *testing.B) { 156 | limitCounter := httprateredis.NewCounter(&httprateredis.Config{ 157 | Host: "localhost", 158 | Port: 6379, 159 | DBIndex: 0, 160 | ClientName: "httprateredis_test", 161 | PrefixKey: fmt.Sprintf("httprate:test:%v", rand.Int31n(100000)), // Unique key for each test 162 | MaxActive: 10, 163 | MaxIdle: 0, 164 | FallbackDisabled: true, 165 | FallbackTimeout: 5 * time.Second, 166 | }) 167 | defer limitCounter.Close() 168 | 169 | limitCounter.Config(1000, time.Minute) 170 | 171 | currentWindow := time.Now().UTC().Truncate(time.Minute) 172 | previousWindow := currentWindow.Add(-time.Minute) 173 | 174 | concurrentRequests := 100 175 | 176 | b.ResetTimer() 177 | 178 | for i := 0; i < b.N; i++ { 179 | for i := range []int{0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 0, 1, 0} { 180 | // Simulate time. 181 | currentWindow.Add(time.Duration(i) * time.Minute) 182 | previousWindow.Add(time.Duration(i) * time.Minute) 183 | 184 | wg := sync.WaitGroup{} 185 | wg.Add(concurrentRequests) 186 | for i := 0; i < concurrentRequests; i++ { 187 | // Simulate concurrent requests with different rate-limit keys. 188 | go func(i int) { 189 | defer wg.Done() 190 | 191 | _, _, _ = limitCounter.Get(fmt.Sprintf("key:%v", i), currentWindow, previousWindow) 192 | _ = limitCounter.IncrementBy(fmt.Sprintf("key:%v", i), currentWindow, rand.Intn(20)) 193 | }(i) 194 | } 195 | wg.Wait() 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /local_fallback_test.go: -------------------------------------------------------------------------------- 1 | package httprateredis_test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strconv" 7 | "testing" 8 | "time" 9 | 10 | "github.com/alicebob/miniredis/v2" 11 | httprateredis "github.com/go-chi/httprate-redis" 12 | ) 13 | 14 | // Test local in-memory counter fallback, which gets activated in case Redis is not available. 15 | func TestLocalFallback(t *testing.T) { 16 | redis, err := miniredis.Run() 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | redisPort, _ := strconv.Atoi(redis.Port()) 21 | 22 | var onErrorCalled bool 23 | var onFallbackCalled bool 24 | 25 | limitCounter := httprateredis.NewCounter(&httprateredis.Config{ 26 | Host: redis.Host(), 27 | Port: uint16(redisPort), 28 | MaxIdle: 0, 29 | MaxActive: 1, 30 | ClientName: "httprateredis_test", 31 | PrefixKey: fmt.Sprintf("httprate:test:%v", rand.Int31n(100000)), // Unique Redis key for each test 32 | FallbackTimeout: 200 * time.Millisecond, 33 | OnError: func(err error) { onErrorCalled = true }, 34 | OnFallbackChange: func(fallbackActivated bool) { onFallbackCalled = true }, 35 | }) 36 | 37 | limitCounter.Config(1000, time.Minute) 38 | 39 | currentWindow := time.Now().UTC().Truncate(time.Minute) 40 | previousWindow := currentWindow.Add(-time.Minute) 41 | 42 | if limitCounter.IsFallbackActivated() { 43 | t.Error("fallback should not be activated at the beginning") 44 | } 45 | if onErrorCalled { 46 | t.Error("onError() should not be called at the beginning") 47 | } 48 | if onFallbackCalled { 49 | t.Error("onFallback() should not be called before we simulate redis failure") 50 | } 51 | 52 | err = limitCounter.IncrementBy("key:fallback", currentWindow, 1) 53 | if err != nil { 54 | t.Error(err) 55 | } 56 | 57 | _, _, err = limitCounter.Get("key:fallback", currentWindow, previousWindow) 58 | if err != nil { 59 | t.Error(err) 60 | } 61 | 62 | if limitCounter.IsFallbackActivated() { 63 | t.Error("fallback should not be activated before we simulate redis failure") 64 | } 65 | if onErrorCalled { 66 | t.Error("onError() should not be called before we simulate redis failure") 67 | } 68 | if onFallbackCalled { 69 | t.Error("onFallback() should not be called before we simulate redis failure") 70 | } 71 | 72 | redis.Close() 73 | 74 | err = limitCounter.IncrementBy("key:fallback", currentWindow, 1) 75 | if err != nil { 76 | t.Error(err) 77 | } 78 | 79 | _, _, err = limitCounter.Get("key:fallback", currentWindow, previousWindow) 80 | if err != nil { 81 | t.Error(err) 82 | } 83 | 84 | if !limitCounter.IsFallbackActivated() { 85 | t.Error("fallback should be activated after we simulate redis failure") 86 | } 87 | if !onErrorCalled { 88 | t.Error("onError() should be called after we simulate redis failure") 89 | } 90 | if !onFallbackCalled { 91 | t.Error("onFallback() should be called after we simulate redis failure") 92 | } 93 | 94 | } 95 | --------------------------------------------------------------------------------