Skip to content

Commit

Permalink
Fallback to local in-memory counter if Redis is unavailable (#15)
Browse files Browse the repository at this point in the history
* Fallback to local in-memory counter if Redis is unavailable

* Example with in-memory fallback; add Prometheus /metrics endpoint

* Improve naming, add example CURL command

* Fix passing custom redis client

* Update go-redis to @latest

* Set DisableIndentity to workaround SETINFO error

redis mget failed: ERR Unknown subcommand or wrong number of arguments for 'setinfo'. Try CLIENT HELP.

* Run CI test 10x times

* Test against in-memory fallback disabled

* CI: Print ulimit

* Adjust timeout values

* Tweak the reconnect interval

* Relax the default fallback timeout a bit
  • Loading branch information
VojtechVitek authored Aug 5, 2024
1 parent 71c932d commit 487f95a
Show file tree
Hide file tree
Showing 10 changed files with 415 additions and 108 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,8 @@ jobs:
- name: Build example
run: cd ./_example && go build -v ./

- name: Check ulimit
run: ulimit -n

- name: Test
run: go test -v ./...
run: go test -v -count=10 ./...
22 changes: 19 additions & 3 deletions _example/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,26 @@ go 1.22.5
replace github.com/go-chi/httprate-redis => ../

require (
github.com/go-chi/chi/v5 v5.1.0
github.com/go-chi/httprate v0.12.0
github.com/go-chi/httprate-redis v0.3.0
github.com/go-chi/telemetry v0.3.4
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-chi/chi/v5 v5.1.0 // indirect
github.com/go-chi/httprate v0.11.0 // indirect
github.com/go-chi/httprate-redis v0.3.0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.19.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.52.3 // indirect
github.com/prometheus/procfs v0.13.0 // indirect
github.com/redis/go-redis/v9 v9.6.1 // indirect
github.com/twmb/murmur3 v1.1.8 // indirect
github.com/uber-go/tally/v4 v4.1.16 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/sys v0.19.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
)
199 changes: 195 additions & 4 deletions _example/go.sum

Large diffs are not rendered by default.

34 changes: 28 additions & 6 deletions _example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,53 @@ import (
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/httprate"
httprateredis "github.com/go-chi/httprate-redis"
"github.com/go-chi/telemetry"
)

func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)

// Expose Prometheus endpoint at /metrics path.
r.Use(telemetry.Collector(telemetry.Config{AllowAny: true}))

rc, _ := httprateredis.NewRedisLimitCounter(&httprateredis.Config{
Host: "127.0.0.1", Port: 6379,
})

r.Group(func(r chi.Router) {
// Set an extra header demonstrating which backend is currently
// in use (redis vs. local in-memory fallback).
r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if rc.IsFallbackActivated() {
w.Header().Set("X-RateLimit-Backend", "in-memory")
} else {
w.Header().Set("X-RateLimit-Backend", "redis")
}
next.ServeHTTP(w, r)
})
})

// Rate-limit at 50 req/s per IP address.
r.Use(httprate.Limit(
5,
time.Minute,
50, time.Second,
httprate.WithKeyByIP(),
httprateredis.WithRedisLimitCounter(&httprateredis.Config{
Host: "127.0.0.1", Port: 6379,
}),
httprate.WithLimitCounter(rc),
))

r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("5 req/min\n"))
w.Write([]byte("ok\n"))
})
})

log.Printf("Serving at localhost:3333")
log.Printf("Serving at http://localhost:3333, rate-limited at 50 req/s per IP address")
log.Println()
log.Printf("Try running:")
log.Printf("curl -v http://localhost:3333")
log.Printf("Try making 55 requests:")
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]"`)

http.ListenAndServe(":3333", r)
}
36 changes: 36 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package httprateredis

import (
"time"

"github.com/redis/go-redis/v9"
)

type Config struct {
Disabled bool `toml:"disabled"` // default: false

WindowLength time.Duration `toml:"window_length"` // default: 1m
ClientName string `toml:"client_name"` // default: os.Args[0]
PrefixKey string `toml:"prefix_key"` // default: "httprate"

// Disable the use of the local in-memory fallback mechanism. When enabled,
// the system will return HTTP 428 for all requests when Redis is down.
FallbackDisabled bool `toml:"fallback_disabled"` // default: false

// Timeout for each Redis command after which we fall back to a local
// in-memory counter. If Redis does not respond within this duration,
// the system will use the local counter unless it is explicitly disabled.
FallbackTimeout time.Duration `toml:"fallback_timeout"` // default: 50ms

// Client if supplied will be used and the below fields will be ignored.
//
// NOTE: It's recommended to set short dial/read/write timeouts and disable
// retries on the client, so the local in-memory fallback can activate quickly.
Client *redis.Client `toml:"-"`
Host string `toml:"host"`
Port uint16 `toml:"port"`
Password string `toml:"password"` // optional
DBIndex int `toml:"db_index"` // default: 0
MaxIdle int `toml:"max_idle"` // default: 4
MaxActive int `toml:"max_active"` // default: 8
}
18 changes: 0 additions & 18 deletions conn.go

This file was deleted.

6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ module github.com/go-chi/httprate-redis
go 1.19

require (
github.com/go-chi/httprate v0.9.0
github.com/redis/go-redis/v9 v9.6.0
github.com/go-chi/httprate v0.12.0
github.com/redis/go-redis/v9 v9.6.1
golang.org/x/sync v0.7.0
)

require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
golang.org/x/sync v0.7.0 // indirect
)
12 changes: 4 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/go-chi/httprate v0.8.0 h1:CyKng28yhGnlGXH9EDGC/Qizj29afJQSNW15W/yj34o=
github.com/go-chi/httprate v0.8.0/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A=
github.com/go-chi/httprate v0.9.0 h1:21A+4WDMDA5FyWcg7mNrhj63aNT8CGh+Z1alOE/piU8=
github.com/go-chi/httprate v0.9.0/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A=
github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/go-chi/httprate v0.12.0 h1:08D/te3pOTJe5+VAZTQrHxwdsH2NyliiUoRD1naKaMg=
github.com/go-chi/httprate v0.12.0/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0=
github.com/redis/go-redis/v9 v9.6.0 h1:NLck+Rab3AOTHw21CGRpvQpgTrAU4sgdCswqGtlhGRA=
github.com/redis/go-redis/v9 v9.6.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
Loading

0 comments on commit 487f95a

Please sign in to comment.