refactor: rework limiter, traffic accounting, and logging (nlog)

This commit is contained in:
xboard
2026-03-23 23:01:13 +08:00
parent 4676caac96
commit 3f1234a05c
20 changed files with 1721 additions and 1333 deletions
+22 -6
View File
@@ -1,7 +1,6 @@
package limiter
import (
"log/slog"
"sync"
"sync/atomic"
@@ -9,6 +8,9 @@ import (
"golang.org/x/time/rate"
)
// SpeedTrackerLogCallback is called when bucket updates occur.
type SpeedTrackerLogCallback func(msg string)
// SpeedTracker manages per-user token-bucket rate limiters.
// It does NOT wrap connections itself — instead, ConnTracker consults it
// via GetLimiter to embed rate limiting in the same tracked connection
@@ -22,6 +24,9 @@ type SpeedTracker struct {
// Fast-path: when no users have a speed limit, GetLimiter returns nil
// immediately without any map lookup.
hasLimits atomic.Bool
// Optional callback for logging
logFunc SpeedTrackerLogCallback
}
// NewSpeedTracker creates a bucket manager for per-user bandwidth throttling.
@@ -33,19 +38,23 @@ func NewSpeedTracker(l *Limiter) *SpeedTracker {
}
}
// SetLogCallback sets the logging callback.
func (t *SpeedTracker) SetLogCallback(f SpeedTrackerLogCallback) {
t.logFunc = f
}
// UpdateBuckets performs an incremental update of rate limiter buckets.
// It reuses existing rate.Limiter instances to avoid burst resets and
// minimizes memory allocations.
func (t *SpeedTracker) UpdateBuckets() {
currentUsers := make([]panel.User, 0, 32)
t.limiter.mu.RLock()
currentUsers := make([]panel.User, 0, len(t.limiter.users))
for _, u := range t.limiter.users {
currentUsers = append(currentUsers, u)
}
t.limiter.mu.RUnlock()
t.mu.Lock()
defer t.mu.Unlock()
newUUIDMap := make(map[string]int, len(currentUsers))
activeIDs := make(map[int]struct{}, len(currentUsers))
@@ -71,8 +80,6 @@ func (t *SpeedTracker) UpdateBuckets() {
}
if existing, ok := t.buckets[user.ID]; ok {
// Thread-safe in-place update. Existing connections continue
// using this limiter with the new rate immediately.
existing.SetLimit(rate.Limit(bytesPerSec))
existing.SetBurst(burst)
} else {
@@ -89,7 +96,16 @@ func (t *SpeedTracker) UpdateBuckets() {
t.uuidMap = newUUIDMap
t.hasLimits.Store(len(t.buckets) > 0)
slog.Info("speedtracker: buckets updated", "active_limiters", len(t.buckets))
// IMPORTANT: logFunc must run after Unlock. The callback installed in
// service.startKernel calls LimitedUserCount(), which needs RLock on t.mu.
// Calling it while this goroutine holds Lock causes a self-deadlock on
// sync.RWMutex (writer blocked forever waiting for itself to release).
t.mu.Unlock()
if t.logFunc != nil {
t.logFunc("buckets updated")
}
}
// GetLimiter returns the rate limiter for the given user UUID, or nil if