Mining/pkg/mining/ratelimiter.go
Claude 95c602104f
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run
ax(batch): rename abbreviated locals in source files
ns→nodeService, c→xmrigConfig, m→manager, k→minerKey, s→stringValue,
l→linesParam, ip→clientAddress, r→character, mgr→manager across
pkg/mining and cmd/mining source files.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-02 18:36:02 +01:00

121 lines
2.8 KiB
Go

package mining
import (
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
)
// limiter := NewRateLimiter(10, 20) // 10 req/s, burst 20
// router.Use(limiter.Middleware())
type RateLimiter struct {
requestsPerSecond int
burst int
clients map[string]*rateLimitClient
mutex sync.RWMutex
stopChan chan struct{}
stopped bool
}
type rateLimitClient struct {
tokens float64
lastCheck time.Time
}
// limiter := NewRateLimiter(10, 20) // 10 requests/second, burst of 20
// defer limiter.Stop()
func NewRateLimiter(requestsPerSecond, burst int) *RateLimiter {
limiter := &RateLimiter{
requestsPerSecond: requestsPerSecond,
burst: burst,
clients: make(map[string]*rateLimitClient),
stopChan: make(chan struct{}),
}
// Start cleanup goroutine
go limiter.cleanupLoop()
return limiter
}
// go limiter.cleanupLoop() // started by NewRateLimiter; runs until limiter.Stop()
func (limiter *RateLimiter) cleanupLoop() {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for {
select {
case <-limiter.stopChan:
return
case <-ticker.C:
limiter.cleanup()
}
}
}
// limiter.cleanup() // called every minute by cleanupLoop; evicts IPs idle for >5 minutes
func (limiter *RateLimiter) cleanup() {
limiter.mutex.Lock()
defer limiter.mutex.Unlock()
for ip, client := range limiter.clients {
if time.Since(client.lastCheck) > 5*time.Minute {
delete(limiter.clients, ip)
}
}
}
// limiter.Stop() // call on shutdown to release the cleanup goroutine
func (limiter *RateLimiter) Stop() {
limiter.mutex.Lock()
defer limiter.mutex.Unlock()
if !limiter.stopped {
close(limiter.stopChan)
limiter.stopped = true
}
}
// router.Use(limiter.Middleware()) // install before route handlers
func (limiter *RateLimiter) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
clientAddress := c.ClientIP()
limiter.mutex.Lock()
client, exists := limiter.clients[clientAddress]
if !exists {
client = &rateLimitClient{tokens: float64(limiter.burst), lastCheck: time.Now()}
limiter.clients[clientAddress] = client
}
// Token bucket algorithm
now := time.Now()
elapsed := now.Sub(client.lastCheck).Seconds()
client.tokens += elapsed * float64(limiter.requestsPerSecond)
if client.tokens > float64(limiter.burst) {
client.tokens = float64(limiter.burst)
}
client.lastCheck = now
if client.tokens < 1 {
limiter.mutex.Unlock()
respondWithError(c, http.StatusTooManyRequests, "RATE_LIMITED",
"too many requests", "rate limit exceeded")
c.Abort()
return
}
client.tokens--
limiter.mutex.Unlock()
c.Next()
}
}
// if limiter.ClientCount() == 0 { /* no active clients */ }
func (limiter *RateLimiter) ClientCount() int {
limiter.mutex.RLock()
defer limiter.mutex.RUnlock()
return len(limiter.clients)
}