Skip to content

Commit a8483bb

Browse files
authored
Perf: Increase speed and reduce memory allocations (#14)
1 parent 2e6d1f7 commit a8483bb

File tree

6 files changed

+432
-107
lines changed

6 files changed

+432
-107
lines changed

debug.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,24 @@ import (
55
"sync/atomic"
66
)
77

8+
const (
9+
msgRateLimitExpired = "ratelimit (expired): %s | last count [%d]"
10+
msgDebugEnabled = "rate5 debug enabled"
11+
msgRateLimitedRst = "ratelimit for %s has been reset"
12+
msgRateLimitedNew = "ratelimit %s (new) "
13+
msgRateLimited = "ratelimit %s: last count %d. time: %s"
14+
msgRateLimitStrict = "%s ratelimit for %s: last count %d. time: %s"
15+
)
16+
817
func (q *Limiter) debugPrintf(format string, a ...interface{}) {
918
if atomic.CompareAndSwapUint32(&q.debug, DebugDisabled, DebugDisabled) {
1019
return
1120
}
21+
if len(a) == 2 {
22+
if _, ok := a[1].(*atomic.Int64); ok {
23+
a[1] = a[1].(*atomic.Int64).Load()
24+
}
25+
}
1226
msg := fmt.Sprintf(format, a...)
1327
select {
1428
case q.debugChannel <- msg:
@@ -21,15 +35,15 @@ func (q *Limiter) debugPrintf(format string, a ...interface{}) {
2135

2236
func (q *Limiter) setDebugEvict() {
2337
q.Patrons.OnEvicted(func(src string, count interface{}) {
24-
q.debugPrintf("ratelimit (expired): %s | last count [%d]", src, count)
38+
q.debugPrintf(msgRateLimitExpired, src, count.(*atomic.Int64).Load())
2539
})
2640
}
2741

2842
func (q *Limiter) SetDebug(on bool) {
2943
switch on {
3044
case true:
3145
if atomic.CompareAndSwapUint32(&q.debug, DebugDisabled, DebugEnabled) {
32-
q.debugPrintf("rate5 debug enabled")
46+
q.debugPrintf(msgDebugEnabled)
3347
}
3448
case false:
3549
atomic.CompareAndSwapUint32(&q.debug, DebugEnabled, DebugDisabled)

models.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package rate5
33
import (
44
"fmt"
55
"sync"
6+
"sync/atomic"
67

78
"github.com/patrickmn/go-cache"
89
)
@@ -46,7 +47,7 @@ type Limiter struct {
4647
debug uint32
4748
debugChannel chan string
4849
debugLost int64
49-
known map[interface{}]*int64
50+
known map[interface{}]*atomic.Int64
5051
debugMutex *sync.RWMutex
5152
*sync.RWMutex
5253
}

ratelimiter.go

Lines changed: 65 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,29 @@ import (
99
"github.com/patrickmn/go-cache"
1010
)
1111

12+
const (
13+
strictPrefix = "strict"
14+
hardcorePrefix = "hardcore"
15+
)
16+
17+
var _counters = &sync.Pool{
18+
New: func() interface{} {
19+
i := &atomic.Int64{}
20+
i.Store(0)
21+
return i
22+
},
23+
}
24+
25+
func getCounter() *atomic.Int64 {
26+
got := _counters.Get().(*atomic.Int64)
27+
got.Store(0)
28+
return got
29+
}
30+
31+
func putCounter(i *atomic.Int64) {
32+
_counters.Put(i)
33+
}
34+
1235
/*NewDefaultLimiter returns a ratelimiter with default settings without Strict mode.
1336
* Default window: 25 seconds
1437
* Default burst: 25 requests */
@@ -70,28 +93,40 @@ func NewHardcoreLimiter(window int, burst int) *Limiter {
7093
return l
7194
}
7295

96+
// ResetItem removes an Identity from the limiter's cache.
97+
// This effectively resets the rate limit for the Identity.
7398
func (q *Limiter) ResetItem(from Identity) {
7499
q.Patrons.Delete(from.UniqueKey())
75-
q.debugPrintf("ratelimit for %s has been reset", from.UniqueKey())
100+
q.debugPrintf(msgRateLimitedRst, from.UniqueKey())
101+
}
102+
103+
func (q *Limiter) onEvict(src string, count interface{}) {
104+
q.debugPrintf(msgRateLimitExpired, src, count)
105+
putCounter(count.(*atomic.Int64))
106+
76107
}
77108

78109
func newLimiter(policy Policy) *Limiter {
79110
window := time.Duration(policy.Window) * time.Second
80-
return &Limiter{
111+
q := &Limiter{
81112
Ruleset: policy,
82113
Patrons: cache.New(window, time.Duration(policy.Window)*time.Second),
83-
known: make(map[interface{}]*int64),
114+
known: make(map[interface{}]*atomic.Int64),
84115
RWMutex: &sync.RWMutex{},
85116
debugMutex: &sync.RWMutex{},
86117
debug: DebugDisabled,
87118
}
119+
q.Patrons.OnEvicted(q.onEvict)
120+
return q
88121
}
89122

90-
func intPtr(i int64) *int64 {
91-
return &i
123+
func intPtr(i int64) *atomic.Int64 {
124+
a := getCounter()
125+
a.Store(i)
126+
return a
92127
}
93128

94-
func (q *Limiter) getHitsPtr(src string) *int64 {
129+
func (q *Limiter) getHitsPtr(src string) *atomic.Int64 {
95130
q.RLock()
96131
if _, ok := q.known[src]; ok {
97132
oldPtr := q.known[src]
@@ -100,29 +135,29 @@ func (q *Limiter) getHitsPtr(src string) *int64 {
100135
}
101136
q.RUnlock()
102137
q.Lock()
103-
newPtr := intPtr(0)
138+
newPtr := getCounter()
104139
q.known[src] = newPtr
105140
q.Unlock()
106141
return newPtr
107142
}
108143

109-
func (q *Limiter) strictLogic(src string, count int64) {
144+
func (q *Limiter) strictLogic(src string, count *atomic.Int64) {
110145
knownHits := q.getHitsPtr(src)
111-
atomic.AddInt64(knownHits, 1)
146+
knownHits.Add(1)
112147
var extwindow int64
113-
prefix := "hardcore"
148+
prefix := hardcorePrefix
114149
switch {
115150
case q.Ruleset.Hardcore && q.Ruleset.Window > 1:
116-
extwindow = atomic.LoadInt64(knownHits) * q.Ruleset.Window
151+
extwindow = knownHits.Load() * q.Ruleset.Window
117152
case q.Ruleset.Hardcore && q.Ruleset.Window <= 1:
118-
extwindow = atomic.LoadInt64(knownHits) * 2
153+
extwindow = knownHits.Load() * 2
119154
case !q.Ruleset.Hardcore:
120-
prefix = "strict"
121-
extwindow = atomic.LoadInt64(knownHits) + q.Ruleset.Window
155+
prefix = strictPrefix
156+
extwindow = knownHits.Load() + q.Ruleset.Window
122157
}
123158
exttime := time.Duration(extwindow) * time.Second
124159
_ = q.Patrons.Replace(src, count, exttime)
125-
q.debugPrintf("%s ratelimit for %s: last count %d. time: %s", prefix, src, count, exttime)
160+
q.debugPrintf(msgRateLimitStrict, prefix, src, count.Load(), exttime)
126161
}
127162

128163
func (q *Limiter) CheckStringer(from fmt.Stringer) bool {
@@ -133,33 +168,32 @@ func (q *Limiter) CheckStringer(from fmt.Stringer) bool {
133168
// Check checks and increments an Identities UniqueKey() output against a list of cached strings to determine and raise it's ratelimitting status.
134169
func (q *Limiter) Check(from Identity) (limited bool) {
135170
var count int64
136-
var err error
137-
src := from.UniqueKey()
138-
count, err = q.Patrons.IncrementInt64(src, 1)
139-
if err != nil {
140-
// IncrementInt64 should only error if the value is not an int64, so we can assume it's a new key.
141-
q.debugPrintf("ratelimit %s (new) ", src)
171+
aval, ok := q.Patrons.Get(from.UniqueKey())
172+
switch {
173+
case !ok:
174+
q.debugPrintf(msgRateLimitedNew, from.UniqueKey())
175+
aval = intPtr(1)
142176
// We can't reproduce this throwing an error, we can only assume that the key is new.
143-
_ = q.Patrons.Add(src, int64(1), time.Duration(q.Ruleset.Window)*time.Second)
144-
return false
145-
}
146-
if count < q.Ruleset.Burst {
177+
_ = q.Patrons.Add(from.UniqueKey(), aval, time.Duration(q.Ruleset.Window)*time.Second)
147178
return false
179+
case aval != nil:
180+
count = aval.(*atomic.Int64).Add(1)
181+
if count < q.Ruleset.Burst {
182+
return false
183+
}
148184
}
149185
if q.Ruleset.Strict {
150-
q.strictLogic(src, count)
151-
} else {
152-
q.debugPrintf("ratelimit %s: last count %d. time: %s",
153-
src, count, time.Duration(q.Ruleset.Window)*time.Second)
186+
q.strictLogic(from.UniqueKey(), aval.(*atomic.Int64))
187+
return true
154188
}
189+
q.debugPrintf(msgRateLimited, from.UniqueKey(), count, time.Duration(q.Ruleset.Window)*time.Second)
155190
return true
156191
}
157192

158193
// Peek checks an Identities UniqueKey() output against a list of cached strings to determine ratelimitting status without adding to its request count.
159194
func (q *Limiter) Peek(from Identity) bool {
160-
q.Patrons.DeleteExpired()
161195
if ct, ok := q.Patrons.Get(from.UniqueKey()); ok {
162-
count := ct.(int64)
196+
count := ct.(*atomic.Int64).Load()
163197
if count > q.Ruleset.Burst {
164198
return true
165199
}

0 commit comments

Comments
 (0)