Skip to content

Commit 487f95a

Browse files
authored
Fallback to local in-memory counter if Redis is unavailable (#15)
* 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
1 parent 71c932d commit 487f95a

File tree

10 files changed

+415
-108
lines changed

10 files changed

+415
-108
lines changed

.github/workflows/ci.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,8 @@ jobs:
3333
- name: Build example
3434
run: cd ./_example && go build -v ./
3535

36+
- name: Check ulimit
37+
run: ulimit -n
38+
3639
- name: Test
37-
run: go test -v ./...
40+
run: go test -v -count=10 ./...

_example/go.mod

+19-3
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,26 @@ go 1.22.5
55
replace github.com/go-chi/httprate-redis => ../
66

77
require (
8+
github.com/go-chi/chi/v5 v5.1.0
9+
github.com/go-chi/httprate v0.12.0
10+
github.com/go-chi/httprate-redis v0.3.0
11+
github.com/go-chi/telemetry v0.3.4
12+
)
13+
14+
require (
15+
github.com/beorn7/perks v1.0.1 // indirect
816
github.com/cespare/xxhash/v2 v2.3.0 // indirect
917
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
10-
github.com/go-chi/chi/v5 v5.1.0 // indirect
11-
github.com/go-chi/httprate v0.11.0 // indirect
12-
github.com/go-chi/httprate-redis v0.3.0 // indirect
18+
github.com/golang/mock v1.6.0 // indirect
19+
github.com/pkg/errors v0.9.1 // indirect
20+
github.com/prometheus/client_golang v1.19.0 // indirect
21+
github.com/prometheus/client_model v0.6.1 // indirect
22+
github.com/prometheus/common v0.52.3 // indirect
23+
github.com/prometheus/procfs v0.13.0 // indirect
1324
github.com/redis/go-redis/v9 v9.6.1 // indirect
25+
github.com/twmb/murmur3 v1.1.8 // indirect
26+
github.com/uber-go/tally/v4 v4.1.16 // indirect
27+
go.uber.org/atomic v1.11.0 // indirect
28+
golang.org/x/sys v0.19.0 // indirect
29+
google.golang.org/protobuf v1.33.0 // indirect
1430
)

_example/go.sum

+195-4
Large diffs are not rendered by default.

_example/main.go

+28-6
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,53 @@ import (
99
"github.com/go-chi/chi/v5/middleware"
1010
"github.com/go-chi/httprate"
1111
httprateredis "github.com/go-chi/httprate-redis"
12+
"github.com/go-chi/telemetry"
1213
)
1314

1415
func main() {
1516
r := chi.NewRouter()
1617
r.Use(middleware.Logger)
1718

19+
// Expose Prometheus endpoint at /metrics path.
20+
r.Use(telemetry.Collector(telemetry.Config{AllowAny: true}))
21+
22+
rc, _ := httprateredis.NewRedisLimitCounter(&httprateredis.Config{
23+
Host: "127.0.0.1", Port: 6379,
24+
})
25+
1826
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.
1941
r.Use(httprate.Limit(
20-
5,
21-
time.Minute,
42+
50, time.Second,
2243
httprate.WithKeyByIP(),
2344
httprateredis.WithRedisLimitCounter(&httprateredis.Config{
2445
Host: "127.0.0.1", Port: 6379,
2546
}),
47+
httprate.WithLimitCounter(rc),
2648
))
2749

2850
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
29-
w.Write([]byte("5 req/min\n"))
51+
w.Write([]byte("ok\n"))
3052
})
3153
})
3254

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

3860
http.ListenAndServe(":3333", r)
3961
}

config.go

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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: os.Args[0]
14+
PrefixKey string `toml:"prefix_key"` // default: "httprate"
15+
16+
// Disable the use of the local in-memory fallback mechanism. When enabled,
17+
// the system will return HTTP 428 for all requests when Redis is down.
18+
FallbackDisabled bool `toml:"fallback_disabled"` // default: false
19+
20+
// Timeout for each Redis command after which we fall back to a local
21+
// in-memory counter. If Redis does not respond within this duration,
22+
// the system will use the local counter unless it is explicitly disabled.
23+
FallbackTimeout time.Duration `toml:"fallback_timeout"` // default: 50ms
24+
25+
// Client if supplied will be used and the below fields will be ignored.
26+
//
27+
// NOTE: It's recommended to set short dial/read/write timeouts and disable
28+
// retries on the client, so the local in-memory fallback can activate quickly.
29+
Client *redis.Client `toml:"-"`
30+
Host string `toml:"host"`
31+
Port uint16 `toml:"port"`
32+
Password string `toml:"password"` // optional
33+
DBIndex int `toml:"db_index"` // default: 0
34+
MaxIdle int `toml:"max_idle"` // default: 4
35+
MaxActive int `toml:"max_active"` // default: 8
36+
}

conn.go

-18
This file was deleted.

go.mod

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ module github.com/go-chi/httprate-redis
33
go 1.19
44

55
require (
6-
github.com/go-chi/httprate v0.9.0
7-
github.com/redis/go-redis/v9 v9.6.0
6+
github.com/go-chi/httprate v0.12.0
7+
github.com/redis/go-redis/v9 v9.6.1
8+
golang.org/x/sync v0.7.0
89
)
910

1011
require (
1112
github.com/cespare/xxhash/v2 v2.3.0 // indirect
1213
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
13-
golang.org/x/sync v0.7.0 // indirect
1414
)

go.sum

+4-8
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
22
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
3-
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
4-
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
53
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
64
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
75
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
86
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
9-
github.com/go-chi/httprate v0.8.0 h1:CyKng28yhGnlGXH9EDGC/Qizj29afJQSNW15W/yj34o=
10-
github.com/go-chi/httprate v0.8.0/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A=
11-
github.com/go-chi/httprate v0.9.0 h1:21A+4WDMDA5FyWcg7mNrhj63aNT8CGh+Z1alOE/piU8=
12-
github.com/go-chi/httprate v0.9.0/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A=
13-
github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
14-
github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
7+
github.com/go-chi/httprate v0.12.0 h1:08D/te3pOTJe5+VAZTQrHxwdsH2NyliiUoRD1naKaMg=
8+
github.com/go-chi/httprate v0.12.0/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0=
159
github.com/redis/go-redis/v9 v9.6.0 h1:NLck+Rab3AOTHw21CGRpvQpgTrAU4sgdCswqGtlhGRA=
1610
github.com/redis/go-redis/v9 v9.6.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
11+
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
12+
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
1713
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
1814
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=

0 commit comments

Comments
 (0)