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:
Ethan Wellenreiter 2025-04-30 20:57:20 -04:00
parent 559aa17da4
commit aed333d04c
4 changed files with 179 additions and 0 deletions

View 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()
}

View 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
}

View 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)
}

View 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()
}
}