159 lines
3.9 KiB
Go
159 lines
3.9 KiB
Go
// steamcache/bandwidth.go
|
|
// Per-client fair-share / absolute bandwidth shaping for table-tier uplink.
|
|
// Distinct from max_requests_per_client concurrency (semaphores in ratelimit.go).
|
|
package steamcache
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"sync"
|
|
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
const bandwidthWriteChunk = 32 * 1024
|
|
|
|
// clientBandwidthLimiter fair-shares uplinkBytesPerSec among active clients and/or
|
|
// applies an absolute per-client bytes/sec cap. Both 0 disables shaping.
|
|
type clientBandwidthLimiter struct {
|
|
uplinkBytesPerSec int64
|
|
absoluteCap int64
|
|
|
|
mu sync.Mutex
|
|
active map[string]int // refcount of in-flight shaped responses per client IP
|
|
limiters map[string]*rate.Limiter
|
|
}
|
|
|
|
func newClientBandwidthLimiter(uplinkBytesPerSec, absoluteCap int64) *clientBandwidthLimiter {
|
|
if uplinkBytesPerSec < 0 {
|
|
uplinkBytesPerSec = 0
|
|
}
|
|
if absoluteCap < 0 {
|
|
absoluteCap = 0
|
|
}
|
|
return &clientBandwidthLimiter{
|
|
uplinkBytesPerSec: uplinkBytesPerSec,
|
|
absoluteCap: absoluteCap,
|
|
active: make(map[string]int),
|
|
limiters: make(map[string]*rate.Limiter),
|
|
}
|
|
}
|
|
|
|
func (b *clientBandwidthLimiter) enabled() bool {
|
|
return b != nil && (b.uplinkBytesPerSec > 0 || b.absoluteCap > 0)
|
|
}
|
|
|
|
// acquire registers clientIP as actively downloading and returns its limiter
|
|
// (nil if shaping disabled) plus a release func that must be deferred.
|
|
func (b *clientBandwidthLimiter) acquire(clientIP string) (*rate.Limiter, func()) {
|
|
if !b.enabled() {
|
|
return nil, func() {}
|
|
}
|
|
b.mu.Lock()
|
|
b.active[clientIP]++
|
|
lim := b.ensureLimiterLocked(clientIP)
|
|
b.recomputeRatesLocked()
|
|
b.mu.Unlock()
|
|
|
|
var once sync.Once
|
|
release := func() {
|
|
once.Do(func() {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
if n := b.active[clientIP]; n <= 1 {
|
|
delete(b.active, clientIP)
|
|
} else {
|
|
b.active[clientIP] = n - 1
|
|
}
|
|
b.recomputeRatesLocked()
|
|
})
|
|
}
|
|
return lim, release
|
|
}
|
|
|
|
func (b *clientBandwidthLimiter) ensureLimiterLocked(clientIP string) *rate.Limiter {
|
|
if lim, ok := b.limiters[clientIP]; ok {
|
|
return lim
|
|
}
|
|
// Start with a placeholder; recomputeRatesLocked sets the real rate.
|
|
lim := rate.NewLimiter(rate.Limit(1), 1)
|
|
b.limiters[clientIP] = lim
|
|
return lim
|
|
}
|
|
|
|
func (b *clientBandwidthLimiter) recomputeRatesLocked() {
|
|
n := len(b.active)
|
|
if n == 0 {
|
|
return
|
|
}
|
|
var fair int64
|
|
if b.uplinkBytesPerSec > 0 {
|
|
fair = b.uplinkBytesPerSec / int64(n)
|
|
if fair < 1 {
|
|
fair = 1
|
|
}
|
|
}
|
|
for ip := range b.active {
|
|
r := fair
|
|
if b.absoluteCap > 0 {
|
|
if r == 0 || b.absoluteCap < r {
|
|
r = b.absoluteCap
|
|
}
|
|
}
|
|
if r < 1 {
|
|
r = 1
|
|
}
|
|
lim := b.ensureLimiterLocked(ip)
|
|
burst := int(r)
|
|
if burst < bandwidthWriteChunk {
|
|
burst = bandwidthWriteChunk
|
|
}
|
|
// Cap burst to avoid huge memory spikes on huge uplinks.
|
|
if burst > 4*bandwidthWriteChunk {
|
|
burst = 4 * bandwidthWriteChunk
|
|
}
|
|
lim.SetLimit(rate.Limit(r))
|
|
lim.SetBurst(burst)
|
|
}
|
|
}
|
|
|
|
// limitedResponseWriter rate-limits response body Write calls. Headers/WriteHeader
|
|
// are unlimited. Implements http.ResponseWriter (+ optional Flusher/Hijacker passthrough
|
|
// is intentionally omitted — SteamCache body path only needs Write).
|
|
type limitedResponseWriter struct {
|
|
http.ResponseWriter
|
|
lim *rate.Limiter
|
|
ctx context.Context
|
|
}
|
|
|
|
func (w *limitedResponseWriter) Write(p []byte) (int, error) {
|
|
if w.lim == nil || len(p) == 0 {
|
|
return w.ResponseWriter.Write(p)
|
|
}
|
|
ctx := w.ctx
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
total := 0
|
|
for total < len(p) {
|
|
chunk := p[total:]
|
|
if len(chunk) > bandwidthWriteChunk {
|
|
chunk = chunk[:bandwidthWriteChunk]
|
|
}
|
|
if err := w.lim.WaitN(ctx, len(chunk)); err != nil {
|
|
return total, err
|
|
}
|
|
n, err := w.ResponseWriter.Write(chunk)
|
|
total += n
|
|
if err != nil {
|
|
return total, err
|
|
}
|
|
}
|
|
return total, nil
|
|
}
|
|
|
|
// Unwrap exposes the underlying ResponseWriter for http.ResponseController etc.
|
|
func (w *limitedResponseWriter) Unwrap() http.ResponseWriter {
|
|
return w.ResponseWriter
|
|
}
|