-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathredis_limit_store.go
More file actions
106 lines (92 loc) · 2.65 KB
/
Copy pathredis_limit_store.go
File metadata and controls
106 lines (92 loc) · 2.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package ratelimiter
import (
"context"
"errors"
"sync"
"time"
"github.com/redis/go-redis/v9"
)
// RedisLimitStore represents internal limiter data store where window counters are
// kept in Redis.
type RedisLimitStore struct {
client redis.UniversalClient
expiration time.Duration
keyPrefix string
timeout time.Duration
closeOnce sync.Once
}
// NewRedisLimitStore creates a new Redis-backed data store for internal limiter data.
func NewRedisLimitStore(client redis.UniversalClient, namespace string, expiration time.Duration) *RedisLimitStore {
return &RedisLimitStore{
client: client,
keyPrefix: namespace,
expiration: expiration,
}
}
// WithTimeout sets a timeout for all Redis commands issued by this store.
// When not set the commands will not have a timeout.
func (r *RedisLimitStore) WithTimeout(timeout time.Duration) *RedisLimitStore {
r.timeout = timeout
return r
}
// Inc increments current window limit counter for key and refreshes its TTL
func (r *RedisLimitStore) Inc(key string, window time.Time) error {
ctx := context.Background()
if r.timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, r.timeout)
defer cancel()
}
k := r.redisKey(key, window)
pipe := r.client.Pipeline()
pipe.Incr(ctx, k)
pipe.Expire(ctx, k, r.expiration)
_, err := pipe.Exec(ctx)
return err
}
// Get gets value of previous window counter and current window counter for key.
func (r *RedisLimitStore) Get(
key string,
previousWindow, currentWindow time.Time,
) (prevValue int64, currValue int64, err error) {
ctx := context.Background()
if r.timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, r.timeout)
defer cancel()
}
pipe := r.client.Pipeline()
prevCmd := pipe.Get(ctx, r.redisKey(key, previousWindow))
currCmd := pipe.Get(ctx, r.redisKey(key, currentWindow))
if _, err := pipe.Exec(ctx); err != nil && !errors.Is(err, redis.Nil) {
return 0, 0, err
}
if prevValue, err = counterValue(prevCmd); err != nil {
return 0, 0, err
}
if currValue, err = counterValue(currCmd); err != nil {
return 0, 0, err
}
return prevValue, currValue, nil
}
func (r *RedisLimitStore) Close() error {
var err error
r.closeOnce.Do(func() {
err = r.client.Close()
})
return err
}
// counterValue reads an int64 counter from a GET command, treating a missing key as zero
func counterValue(cmd *redis.StringCmd) (int64, error) {
v, err := cmd.Int64()
if errors.Is(err, redis.Nil) {
return 0, nil
}
if err != nil {
return 0, err
}
return v, nil
}
func (r *RedisLimitStore) redisKey(key string, window time.Time) string {
return r.keyPrefix + key + "_" + window.Format(time.RFC3339)
}