-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathbucket_limiter.go
More file actions
193 lines (170 loc) · 5.45 KB
/
Copy pathbucket_limiter.go
File metadata and controls
193 lines (170 loc) · 5.45 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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
package ratelimiter
import (
"sync"
"sync/atomic"
"time"
)
// defaultSweepDivisor controls how often the background eviction goroutine runs
// relative to deleteAfter when no explicit interval is configured. An idle
// entry is therefore removed somewhere between deleteAfter and
// deleteAfter*(1 + 1/divisor) after its last use.
const defaultSweepDivisor = 2
// BucketLimiter is a goroutine-safe manager that hands out an independent
// token-bucket [Limiter] per key (for example a user ID or IP address).
//
// Each distinct key receives its own [Limiter], produced by the newLimiter
// factory, so consuming one key's budget never affects another. Limiters are
// created lazily on first access and evicted after they have been idle (not
// accessed through GetOrAdd) for deleteAfter. Eviction runs in a single
// background goroutine started by [NewBucketLimiter] and stopped by
// [BucketLimiter.Close].
//
// The zero value is not usable; construct one with [NewBucketLimiter].
type BucketLimiter[K comparable] struct {
newLimiter func() Limiter
deleteAfter time.Duration
interval time.Duration
now func() time.Time
storage Storage[K, Limiter]
// access tracks the last-use time (unix nanoseconds) per key so the
// sweeper can evict genuinely idle entries. It is kept separate from
// storage so that custom Storage backends only ever hold Limiter values.
access sync.Map // K -> *atomic.Int64
stop chan struct{}
done chan struct{}
closeOnce sync.Once
}
// Option configures a [BucketLimiter] at construction time.
type Option func(*config)
type config struct {
now func() time.Time
interval time.Duration
}
// WithClock overrides the time source used for idle tracking and eviction.
// It is primarily useful in tests to make eviction deterministic.
func WithClock(now func() time.Time) Option {
return func(c *config) {
if now != nil {
c.now = now
}
}
}
// WithSweepInterval overrides how often the background eviction goroutine runs.
// When unset, it defaults to deleteAfter/2. Ignored when deleteAfter <= 0.
func WithSweepInterval(d time.Duration) Option {
return func(c *config) {
if d > 0 {
c.interval = d
}
}
}
// NewBucketLimiter creates a [BucketLimiter].
//
// - newLimiter is called once per new key to build that key's independent
// [Limiter]. Use [NewRateLimiterFunc] for the common *rate.Limiter case.
// - deleteAfter is the idle duration after which an unused key is evicted.
// A value <= 0 disables eviction (limiters live until [BucketLimiter.Remove]
// or [BucketLimiter.Close]); prefer this only for bounded key spaces.
// - storage is the backing store, commonly
// ratelimiter.NewInMemoryStorage[K, ratelimiter.Limiter]().
//
// When eviction is enabled a background goroutine is started; call
// [BucketLimiter.Close] to stop it and release resources.
func NewBucketLimiter[K comparable](
newLimiter func() Limiter,
deleteAfter time.Duration,
storage Storage[K, Limiter],
opts ...Option,
) *BucketLimiter[K] {
cfg := config{now: time.Now}
for _, opt := range opts {
opt(&cfg)
}
interval := cfg.interval
if interval <= 0 {
interval = deleteAfter / defaultSweepDivisor
if interval <= 0 {
interval = deleteAfter
}
}
b := &BucketLimiter[K]{
newLimiter: newLimiter,
deleteAfter: deleteAfter,
interval: interval,
now: cfg.now,
storage: storage,
stop: make(chan struct{}),
done: make(chan struct{}),
}
if deleteAfter > 0 {
go b.sweepLoop()
} else {
close(b.done)
}
return b
}
// GetOrAdd returns the [Limiter] for key, creating and storing a new one via
// the newLimiter factory if none exists yet. Concurrent callers racing on the
// same new key all receive the same instance. Every call refreshes the key's
// idle timer.
func (b *BucketLimiter[K]) GetOrAdd(key K) Limiter {
limiter, ok := b.storage.Load(key)
if !ok {
// LoadOrStore makes creation atomic: if another goroutine wins the
// race, we discard our fresh limiter and use the stored one.
limiter, _ = b.storage.LoadOrStore(key, b.newLimiter())
}
b.touch(key)
return limiter
}
// touch records the current time as key's last-use time.
func (b *BucketLimiter[K]) touch(key K) {
if b.deleteAfter <= 0 {
return
}
v, _ := b.access.LoadOrStore(key, new(atomic.Int64))
v.(*atomic.Int64).Store(b.now().UnixNano())
}
// Remove immediately deletes the limiter for key. A subsequent GetOrAdd
// creates a fresh one.
func (b *BucketLimiter[K]) Remove(key K) {
b.storage.Delete(key)
b.access.Delete(key)
}
// Close stops the background eviction goroutine and waits for it to exit. It is
// safe to call multiple times and from multiple goroutines. After Close the
// manager can still serve GetOrAdd, but idle entries will no longer be evicted
// automatically.
func (b *BucketLimiter[K]) Close() error {
b.closeOnce.Do(func() {
close(b.stop)
})
<-b.done
return nil
}
// sweepLoop periodically evicts entries idle for longer than deleteAfter.
func (b *BucketLimiter[K]) sweepLoop() {
defer close(b.done)
ticker := time.NewTicker(b.interval)
defer ticker.Stop()
for {
select {
case <-b.stop:
return
case <-ticker.C:
b.evictIdle()
}
}
}
// evictIdle removes every key whose last use is older than deleteAfter.
func (b *BucketLimiter[K]) evictIdle() {
cutoff := b.now().Add(-b.deleteAfter).UnixNano()
b.access.Range(func(k, v any) bool {
if v.(*atomic.Int64).Load() <= cutoff {
key := k.(K)
b.storage.Delete(key)
b.access.Delete(key)
}
return true
})
}