Skip to content

Commit 1e7e136

Browse files
authored
Merge pull request #7 from augustus281/DEV
[algo]: sliding window counter
2 parents 8a33247 + 41b2a14 commit 1e7e136

File tree

2 files changed

+114
-0
lines changed

2 files changed

+114
-0
lines changed

sliding_window_counter.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package ratelimiter
2+
3+
import (
4+
"sync"
5+
"time"
6+
)
7+
8+
type SlidingWindowCounter struct {
9+
windowSize time.Duration
10+
maxRequests int
11+
currentWindow int64
12+
requestCount int
13+
previousCount int
14+
mu sync.Mutex
15+
}
16+
17+
func NewSlidingWindowCounter(windowSize time.Duration, maxRequests int) *SlidingWindowCounter {
18+
return &SlidingWindowCounter{
19+
windowSize: windowSize,
20+
maxRequests: maxRequests,
21+
currentWindow: time.Now().Unix() / int64(windowSize.Seconds()),
22+
requestCount: 0,
23+
previousCount: 0,
24+
}
25+
}
26+
27+
func (swc *SlidingWindowCounter) AllowRequest() bool {
28+
swc.mu.Lock()
29+
defer swc.mu.Unlock()
30+
31+
now := time.Now().Unix()
32+
window := now / int64(swc.windowSize.Seconds())
33+
34+
// If we've moved to a new window, update the counts
35+
if window != swc.currentWindow {
36+
swc.previousCount = swc.requestCount
37+
swc.requestCount = 0
38+
swc.currentWindow = window
39+
}
40+
41+
// Calculate the weighted request count
42+
windowElapsed := float64(now%int64(swc.windowSize.Seconds())) / float64(swc.windowSize.Seconds())
43+
threshold := float64(swc.previousCount)*(1-windowElapsed) + float64(swc.requestCount)
44+
45+
// Check if we're within the limit
46+
if threshold < float64(swc.maxRequests) {
47+
swc.requestCount++
48+
return true
49+
}
50+
51+
return false
52+
}

sliding_window_counter_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package ratelimiter
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func newSlidingWindowCounter(windowSize time.Duration, maxRequests int) *SlidingWindowCounter {
9+
return NewSlidingWindowCounter(windowSize, maxRequests)
10+
}
11+
12+
func TestSlidingWindowCounter_AllowRequest(t *testing.T) {
13+
tests := []struct {
14+
windowSize time.Duration
15+
maxRequests int
16+
requests int
17+
expectAllowed bool
18+
}{
19+
{time.Second * 10, 5, 5, true}, // within limit
20+
{time.Second * 10, 5, 6, false}, // exceeding limit
21+
{time.Second * 10, 5, 10, false}, // far exceeding limit
22+
{time.Second * 5, 2, 2, true}, // within smaller window
23+
{time.Second * 5, 2, 3, false}, // exceeding limit in smaller window
24+
}
25+
26+
for _, tt := range tests {
27+
t.Run("", func(t *testing.T) {
28+
swc := newSlidingWindowCounter(tt.windowSize, tt.maxRequests)
29+
30+
for i := 0; i < tt.requests; i++ {
31+
allowed := swc.AllowRequest()
32+
if i < tt.maxRequests && !allowed {
33+
t.Errorf("Request %d was not allowed, but it should be", i)
34+
}
35+
if i >= tt.maxRequests && allowed {
36+
t.Errorf("Request %d was allowed, but it should not be", i)
37+
}
38+
}
39+
})
40+
}
41+
}
42+
43+
func TestSlidingWindowCounter_WindowExpiration(t *testing.T) {
44+
windowSize := time.Second * 2
45+
maxRequests := 2
46+
47+
swc := newSlidingWindowCounter(windowSize, maxRequests)
48+
49+
if !swc.AllowRequest() {
50+
t.Errorf("First request should be allowed")
51+
}
52+
53+
if !swc.AllowRequest() {
54+
t.Errorf("Second request should be allowed")
55+
}
56+
57+
time.Sleep(windowSize + time.Second)
58+
59+
if swc.AllowRequest() {
60+
t.Errorf("Request after window expiration should not be allowed")
61+
}
62+
}

0 commit comments

Comments
 (0)