Files
pipely/internal/middleware/rate_limit.go
T

114 lines
2.3 KiB
Go
Raw Normal View History

2026-07-07 10:29:28 +08:00
package middleware
import (
"sync"
"time"
"github.com/gin-gonic/gin"
)
// TokenBucket implements a token bucket rate limiter per key (e.g. IP or device ID).
type TokenBucket struct {
mu sync.Mutex
buckets map[string]*bucket
rate float64 // tokens per second
capacity float64
}
type bucket struct {
tokens float64
lastFill time.Time
}
func NewTokenBucket(rate, capacity int) *TokenBucket {
return &TokenBucket{
buckets: make(map[string]*bucket),
rate: float64(rate),
capacity: float64(capacity),
}
}
func (tb *TokenBucket) Allow(key string) bool {
tb.mu.Lock()
defer tb.mu.Unlock()
b, ok := tb.buckets[key]
if !ok {
b = &bucket{tokens: tb.capacity, lastFill: time.Now()}
tb.buckets[key] = b
}
now := time.Now()
elapsed := now.Sub(b.lastFill).Seconds()
b.tokens += elapsed * tb.rate
if b.tokens > tb.capacity {
b.tokens = tb.capacity
}
b.lastFill = now
if b.tokens >= 1 {
b.tokens--
return true
}
return false
}
// Cleanup removes stale buckets older than the given duration.
func (tb *TokenBucket) Cleanup(maxAge time.Duration) {
tb.mu.Lock()
defer tb.mu.Unlock()
now := time.Now()
for k, b := range tb.buckets {
if now.Sub(b.lastFill) > maxAge {
delete(tb.buckets, k)
}
}
}
// RateLimitMiddleware returns a Gin middleware that rate-limits requests per client IP.
func RateLimitMiddleware(tb *TokenBucket) gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
if !tb.Allow(ip) {
c.AbortWithStatusJSON(429, gin.H{"error": "Rate limit exceeded. Please slow down."})
return
}
c.Next()
}
}
// GatedSemaphore limits the number of concurrent operations (e.g. downloads).
type GatedSemaphore struct {
ch chan struct{}
}
func NewGatedSemaphore(maxConcurrent int) *GatedSemaphore {
return &GatedSemaphore{ch: make(chan struct{}, maxConcurrent)}
}
func (gs *GatedSemaphore) Acquire() {
gs.ch <- struct{}{}
}
func (gs *GatedSemaphore) Release() {
<-gs.ch
}
func (gs *GatedSemaphore) Available() int {
return cap(gs.ch) - len(gs.ch)
}
// GatedMiddleware only allows requests through if the semaphore has capacity.
func GatedMiddleware(gs *GatedSemaphore) gin.HandlerFunc {
return func(c *gin.Context) {
select {
case gs.ch <- struct{}{}:
defer func() { <-gs.ch }()
c.Next()
default:
c.AbortWithStatusJSON(503, gin.H{"error": "Server at capacity. Please retry later."})
}
}
}