A few rate limiter options. Currently undecided but may just go with the prebuilt one by go-chi
Signed-off-by: Ethan Wellenreiter <ewellenreiter@gmail.com>
This commit is contained in:
parent
559aa17da4
commit
aed333d04c
49
backend/internal/ratelimiter/fixed-window.go
Normal file
49
backend/internal/ratelimiter/fixed-window.go
Normal file
@ -0,0 +1,49 @@
|
||||
package ratelimiter
|
||||
|
||||
// Copied from https://www.youtube.com/watch?v=m5oyY9fgZPs
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FixedWindowRateLimiter struct {
|
||||
sync.RWMutex
|
||||
clients map[string]int
|
||||
limit int
|
||||
window time.Duration
|
||||
}
|
||||
|
||||
func NewFixedWindowLimiter(limit int, window time.Duration) *FixedWindowRateLimiter {
|
||||
return &FixedWindowRateLimiter{
|
||||
clients: make(map[string]int),
|
||||
limit: limit,
|
||||
window: window,
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *FixedWindowRateLimiter) Allow(ip string) (bool, time.Duration) {
|
||||
rl.RLock()
|
||||
count, exists := rl.clients[ip]
|
||||
rl.RUnlock()
|
||||
|
||||
if !exists || count < rl.limit {
|
||||
// begin the reset count for a new window
|
||||
rl.Lock()
|
||||
if !exists {
|
||||
go rl.resetCount(ip)
|
||||
}
|
||||
rl.clients[ip]++
|
||||
rl.Unlock()
|
||||
return true, time.Duration(0)
|
||||
}
|
||||
return false, rl.window
|
||||
|
||||
}
|
||||
|
||||
func (rl *FixedWindowRateLimiter) resetCount(ip string) {
|
||||
time.Sleep(rl.window)
|
||||
rl.Lock()
|
||||
delete(rl.clients, ip)
|
||||
rl.Unlock()
|
||||
}
|
||||
13
backend/internal/ratelimiter/ratelimiter.go
Normal file
13
backend/internal/ratelimiter/ratelimiter.go
Normal file
@ -0,0 +1,13 @@
|
||||
package ratelimiter
|
||||
|
||||
import "time"
|
||||
|
||||
type Limiter interface {
|
||||
Allow(ip string) (bool, time.Duration)
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
RequestsPerTimeFrame int
|
||||
TimeFrame time.Duration
|
||||
Enabled bool
|
||||
}
|
||||
42
backend/internal/ratelimiter/sliding-window.go
Normal file
42
backend/internal/ratelimiter/sliding-window.go
Normal file
@ -0,0 +1,42 @@
|
||||
package ratelimiter
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SlidingWindowRateLimiter struct {
|
||||
sync.RWMutex
|
||||
clients map[string][]time.Time
|
||||
limit int
|
||||
window time.Duration
|
||||
}
|
||||
|
||||
func NewSlidingWindowLimiter(limit int, window time.Duration) *SlidingWindowRateLimiter {
|
||||
return &SlidingWindowRateLimiter{
|
||||
clients: make(map[string][]time.Time),
|
||||
limit: limit,
|
||||
window: window,
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *SlidingWindowRateLimiter) Allow(ip string) (bool, time.Duration) {
|
||||
rl.RLock()
|
||||
defer rl.Unlock()
|
||||
|
||||
// add new request attempt
|
||||
rl.clients[ip] = append(rl.clients[ip], time.Now())
|
||||
|
||||
// remove ones outside the window
|
||||
for len(rl.clients[ip]) > 0 && time.Since(rl.clients[ip][0]) > rl.window {
|
||||
rl.clients[ip] = rl.clients[ip][1:]
|
||||
}
|
||||
|
||||
// do actual check now
|
||||
if len(rl.clients[ip]) > rl.limit {
|
||||
// calc retry wait time
|
||||
retryAfter := rl.window - time.Since(rl.clients[ip][0])
|
||||
return false, time.Duration(retryAfter)
|
||||
}
|
||||
return true, time.Duration(0)
|
||||
}
|
||||
75
backend/internal/ratelimiter/token-bucket.go
Normal file
75
backend/internal/ratelimiter/token-bucket.go
Normal file
@ -0,0 +1,75 @@
|
||||
package ratelimiter
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type TokenBucketRateLimiter struct {
|
||||
sync.RWMutex
|
||||
clients map[string]*Client
|
||||
config Config
|
||||
}
|
||||
|
||||
func NewTokenBucketRateLimiter(config Config) *TokenBucketRateLimiter {
|
||||
rl := &TokenBucketRateLimiter{
|
||||
clients: make(map[string]*Client),
|
||||
config: config,
|
||||
}
|
||||
|
||||
go rl.cleanupClients()
|
||||
|
||||
return rl
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
Limiter *rate.Limiter
|
||||
LastSeen time.Time
|
||||
}
|
||||
|
||||
func (rl *TokenBucketRateLimiter) getClient(ip string) *Client {
|
||||
rl.RLock()
|
||||
_, exists := rl.clients[ip]
|
||||
rl.RUnlock()
|
||||
|
||||
if !exists {
|
||||
limiter := rate.NewLimiter(rate.Every(rl.config.TimeFrame), rl.config.RequestsPerTimeFrame)
|
||||
rl.Lock()
|
||||
rl.clients[ip] = &Client{
|
||||
Limiter: limiter,
|
||||
LastSeen: time.Now()}
|
||||
|
||||
rl.Unlock()
|
||||
}
|
||||
|
||||
client := rl.clients[ip]
|
||||
client.LastSeen = time.Now()
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (rl *TokenBucketRateLimiter) Allow(ip string) (bool, float64) {
|
||||
client := rl.getClient(ip)
|
||||
|
||||
allowed := client.Limiter.Allow()
|
||||
tokens := client.Limiter.Tokens()
|
||||
|
||||
return allowed, tokens
|
||||
}
|
||||
|
||||
func (rl *TokenBucketRateLimiter) cleanupClients() {
|
||||
for {
|
||||
time.Sleep(time.Minute)
|
||||
//log cleaning up clients
|
||||
|
||||
rl.Lock()
|
||||
for ip, client := range rl.clients {
|
||||
if time.Since(client.LastSeen) > 3*time.Minute { // timeout period
|
||||
delete(rl.clients, ip)
|
||||
}
|
||||
}
|
||||
rl.Unlock()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user