Skip to content

Commit 1ab0ec2

Browse files
authored
Fallback on all Redis errors (#16)
* Fallback on all Redis errors * Benchmark: Fail on test error (tee ate the pipe error) * Disable fallback on benchmark test * Tweak redis pool settings for benchmark * Chill the defaults, we don't need that many connections * Benchmark: Close redis client on each test iteration (-count=10) * Update Redis in CI, lower #no of requests * Remove benchmark from CI, as Github Actions limit us too much #16 (comment)
1 parent 487f95a commit 1ab0ec2

File tree

5 files changed

+51
-100
lines changed

5 files changed

+51
-100
lines changed

.github/workflows/benchmark.yml

-57
This file was deleted.

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111

1212
services:
1313
redis:
14-
image: redis:6
14+
image: redis:7
1515
ports:
1616
- 6379:6379
1717

config.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type Config struct {
2020
// Timeout for each Redis command after which we fall back to a local
2121
// in-memory counter. If Redis does not respond within this duration,
2222
// the system will use the local counter unless it is explicitly disabled.
23-
FallbackTimeout time.Duration `toml:"fallback_timeout"` // default: 50ms
23+
FallbackTimeout time.Duration `toml:"fallback_timeout"` // default: 100ms
2424

2525
// Client if supplied will be used and the below fields will be ignored.
2626
//
@@ -31,6 +31,6 @@ type Config struct {
3131
Port uint16 `toml:"port"`
3232
Password string `toml:"password"` // optional
3333
DBIndex int `toml:"db_index"` // default: 0
34-
MaxIdle int `toml:"max_idle"` // default: 4
35-
MaxActive int `toml:"max_active"` // default: 8
34+
MaxIdle int `toml:"max_idle"` // default: 5
35+
MaxActive int `toml:"max_active"` // default: 10
3636
}

httprateredis.go

+29-27
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ package httprateredis
22

33
import (
44
"context"
5-
"errors"
65
"fmt"
7-
"net"
86
"os"
97
"path/filepath"
108
"strconv"
@@ -40,8 +38,13 @@ func NewRedisLimitCounter(cfg *Config) (*redisCounter, error) {
4038
cfg.PrefixKey = "httprate"
4139
}
4240
if cfg.FallbackTimeout == 0 {
43-
// Activate local in-memory fallback fairly quickly, as this would slow down all requests.
44-
cfg.FallbackTimeout = 100 * time.Millisecond
41+
if cfg.FallbackDisabled {
42+
cfg.FallbackTimeout = time.Second
43+
} else {
44+
// Activate local in-memory fallback fairly quickly,
45+
// so we don't slow down incoming requests too much.
46+
cfg.FallbackTimeout = 100 * time.Millisecond
47+
}
4548
}
4649

4750
rc := &redisCounter{
@@ -54,10 +57,10 @@ func NewRedisLimitCounter(cfg *Config) (*redisCounter, error) {
5457
if cfg.Client == nil {
5558
maxIdle, maxActive := cfg.MaxIdle, cfg.MaxActive
5659
if maxIdle < 1 {
57-
maxIdle = 20
60+
maxIdle = 5
5861
}
5962
if maxActive < 1 {
60-
maxActive = 50
63+
maxActive = 10
6164
}
6265

6366
rc.client = redis.NewClient(&redis.Options{
@@ -107,13 +110,8 @@ func (c *redisCounter) IncrementBy(key string, currentWindow time.Time, amount i
107110
return c.fallbackCounter.IncrementBy(key, currentWindow, amount)
108111
}
109112
defer func() {
110-
if err != nil {
111-
// On redis network error, fallback to local in-memory counter.
112-
var netErr net.Error
113-
if errors.As(err, &netErr) || errors.Is(err, redis.ErrClosed) {
114-
c.fallback()
115-
err = c.fallbackCounter.IncrementBy(key, currentWindow, amount)
116-
}
113+
if c.shouldFallback(err) {
114+
err = c.fallbackCounter.IncrementBy(key, currentWindow, amount)
117115
}
118116
}()
119117
}
@@ -147,13 +145,8 @@ func (c *redisCounter) Get(key string, currentWindow, previousWindow time.Time)
147145
return c.fallbackCounter.Get(key, currentWindow, previousWindow)
148146
}
149147
defer func() {
150-
if err != nil {
151-
// On redis network error, fallback to local in-memory counter.
152-
var netErr net.Error
153-
if errors.As(err, &netErr) || errors.Is(err, redis.ErrClosed) {
154-
c.fallback()
155-
curr, prev, err = c.fallbackCounter.Get(key, currentWindow, previousWindow)
156-
}
148+
if c.shouldFallback(err) {
149+
curr, prev, err = c.fallbackCounter.Get(key, currentWindow, previousWindow)
157150
}
158151
}()
159152
}
@@ -189,25 +182,34 @@ func (c *redisCounter) IsFallbackActivated() bool {
189182
return c.fallbackActivated.Load()
190183
}
191184

192-
func (c *redisCounter) fallback() {
193-
// Activate the in-memory counter fallback, unless activated by some other goroutine.
194-
fallbackAlreadyActivated := c.fallbackActivated.Swap(true)
195-
if fallbackAlreadyActivated {
196-
return
185+
func (c *redisCounter) Close() error {
186+
return c.client.Close()
187+
}
188+
189+
func (c *redisCounter) shouldFallback(err error) bool {
190+
if err == nil {
191+
return false
192+
}
193+
194+
// Activate the local in-memory counter fallback, unless activated by some other goroutine.
195+
alreadyActivated := c.fallbackActivated.Swap(true)
196+
if !alreadyActivated {
197+
go c.reconnect()
197198
}
198199

199-
go c.reconnect()
200+
return true
200201
}
201202

202203
func (c *redisCounter) reconnect() {
203204
// Try to re-connect to redis every 200ms.
204205
for {
206+
time.Sleep(200 * time.Millisecond)
207+
205208
err := c.client.Ping(context.Background()).Err()
206209
if err == nil {
207210
c.fallbackActivated.Store(false)
208211
return
209212
}
210-
time.Sleep(200 * time.Millisecond)
211213
}
212214
}
213215

httprateredis_test.go

+18-12
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ func TestRedisCounter(t *testing.T) {
1515
limitCounter, err := httprateredis.NewRedisLimitCounter(&httprateredis.Config{
1616
Host: "localhost",
1717
Port: 6379,
18-
MaxIdle: 100,
19-
MaxActive: 200,
18+
MaxIdle: 0,
19+
MaxActive: 2,
2020
DBIndex: 0,
2121
ClientName: "httprateredis_test",
2222
PrefixKey: fmt.Sprintf("httprate:test:%v", rand.Int31n(100000)), // Unique Redis key for each test
@@ -26,6 +26,7 @@ func TestRedisCounter(t *testing.T) {
2626
if err != nil {
2727
t.Fatalf("redis not available: %v", err)
2828
}
29+
defer limitCounter.Close()
2930

3031
limitCounter.Config(1000, time.Minute)
3132

@@ -156,23 +157,28 @@ func TestRedisCounter(t *testing.T) {
156157

157158
func BenchmarkLocalCounter(b *testing.B) {
158159
limitCounter, err := httprateredis.NewRedisLimitCounter(&httprateredis.Config{
159-
Host: "localhost",
160-
Port: 6379,
161-
MaxIdle: 500,
162-
MaxActive: 500,
163-
DBIndex: 0,
164-
ClientName: "httprateredis_test",
165-
PrefixKey: fmt.Sprintf("httprate:test:%v", rand.Int31n(100000)), // Unique key for each test
160+
Host: "localhost",
161+
Port: 6379,
162+
DBIndex: 0,
163+
ClientName: "httprateredis_test",
164+
PrefixKey: fmt.Sprintf("httprate:test:%v", rand.Int31n(100000)), // Unique key for each test
165+
MaxActive: 10,
166+
MaxIdle: 0,
167+
FallbackDisabled: true,
168+
FallbackTimeout: 5 * time.Second,
166169
})
167170
if err != nil {
168171
b.Fatalf("redis not available: %v", err)
169172
}
173+
defer limitCounter.Close()
170174

171175
limitCounter.Config(1000, time.Minute)
172176

173177
currentWindow := time.Now().UTC().Truncate(time.Minute)
174178
previousWindow := currentWindow.Add(-time.Minute)
175179

180+
concurrentRequests := 100
181+
176182
b.ResetTimer()
177183

178184
for i := 0; i < b.N; i++ {
@@ -182,14 +188,14 @@ func BenchmarkLocalCounter(b *testing.B) {
182188
previousWindow.Add(time.Duration(i) * time.Minute)
183189

184190
wg := sync.WaitGroup{}
185-
wg.Add(1000)
186-
for i := 0; i < 1000; i++ {
191+
wg.Add(concurrentRequests)
192+
for i := 0; i < concurrentRequests; i++ {
187193
// Simulate concurrent requests with different rate-limit keys.
188194
go func(i int) {
189195
defer wg.Done()
190196

191197
_, _, _ = limitCounter.Get(fmt.Sprintf("key:%v", i), currentWindow, previousWindow)
192-
_ = limitCounter.IncrementBy(fmt.Sprintf("key:%v", i), currentWindow, rand.Intn(100))
198+
_ = limitCounter.IncrementBy(fmt.Sprintf("key:%v", i), currentWindow, rand.Intn(20))
193199
}(i)
194200
}
195201
wg.Wait()

0 commit comments

Comments
 (0)