Browse Source
- Add token-bucket bandwidth rate limiting for Blossom uploads - ORLY_BLOSSOM_RATE_LIMIT enables limiting (default: false) - ORLY_BLOSSOM_DAILY_LIMIT_MB sets daily limit (default: 10MB) - ORLY_BLOSSOM_BURST_LIMIT_MB sets burst cap (default: 50MB) - Followed users, admins, owners are exempt (unlimited) - Change emergency mode throttling from exponential to linear scaling - Old: 16x multiplier at emergency threshold entry - New: 1x at threshold, +1x per 20% excess pressure - Reduce follows ACL throttle increment from 200ms to 25ms per event - Update dependencies Files modified: - app/blossom.go: Pass rate limit config to blossom server - app/config/config.go: Add Blossom rate limit config options - pkg/blossom/ratelimit.go: New bandwidth limiter implementation - pkg/blossom/server.go: Add rate limiter integration - pkg/blossom/handlers.go: Check rate limits on upload/mirror/media - pkg/ratelimit/limiter.go: Linear emergency throttling - pkg/acl/follows.go: Reduce default throttle increment Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>main v0.49.0
17 changed files with 321 additions and 44 deletions
@ -0,0 +1,131 @@ |
|||||||
|
package blossom |
||||||
|
|
||||||
|
import ( |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
// BandwidthState tracks upload bandwidth for an identity
|
||||||
|
type BandwidthState struct { |
||||||
|
BucketBytes int64 // Current token bucket level (bytes available)
|
||||||
|
LastUpdate time.Time // Last time bucket was updated
|
||||||
|
} |
||||||
|
|
||||||
|
// BandwidthLimiter implements token bucket rate limiting for uploads.
|
||||||
|
// Each identity gets a bucket that replenishes at dailyLimit/day rate.
|
||||||
|
// Uploads consume tokens from the bucket.
|
||||||
|
type BandwidthLimiter struct { |
||||||
|
mu sync.Mutex |
||||||
|
states map[string]*BandwidthState // keyed by pubkey hex or IP
|
||||||
|
dailyLimit int64 // bytes per day
|
||||||
|
burstLimit int64 // max bucket size (burst capacity)
|
||||||
|
refillRate float64 // bytes per second refill rate
|
||||||
|
} |
||||||
|
|
||||||
|
// NewBandwidthLimiter creates a new bandwidth limiter.
|
||||||
|
// dailyLimitMB is the average daily limit in megabytes.
|
||||||
|
// burstLimitMB is the maximum burst capacity in megabytes.
|
||||||
|
func NewBandwidthLimiter(dailyLimitMB, burstLimitMB int64) *BandwidthLimiter { |
||||||
|
dailyBytes := dailyLimitMB * 1024 * 1024 |
||||||
|
burstBytes := burstLimitMB * 1024 * 1024 |
||||||
|
|
||||||
|
return &BandwidthLimiter{ |
||||||
|
states: make(map[string]*BandwidthState), |
||||||
|
dailyLimit: dailyBytes, |
||||||
|
burstLimit: burstBytes, |
||||||
|
refillRate: float64(dailyBytes) / 86400.0, // bytes per second
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// CheckAndConsume checks if an upload of the given size is allowed for the identity,
|
||||||
|
// and if so, consumes the tokens. Returns true if allowed, false if rate limited.
|
||||||
|
// The identity should be pubkey hex for authenticated users, or IP for anonymous.
|
||||||
|
func (bl *BandwidthLimiter) CheckAndConsume(identity string, sizeBytes int64) bool { |
||||||
|
bl.mu.Lock() |
||||||
|
defer bl.mu.Unlock() |
||||||
|
|
||||||
|
now := time.Now() |
||||||
|
state, exists := bl.states[identity] |
||||||
|
|
||||||
|
if !exists { |
||||||
|
// New identity starts with full burst capacity
|
||||||
|
state = &BandwidthState{ |
||||||
|
BucketBytes: bl.burstLimit, |
||||||
|
LastUpdate: now, |
||||||
|
} |
||||||
|
bl.states[identity] = state |
||||||
|
} else { |
||||||
|
// Refill bucket based on elapsed time
|
||||||
|
elapsed := now.Sub(state.LastUpdate).Seconds() |
||||||
|
refill := int64(elapsed * bl.refillRate) |
||||||
|
state.BucketBytes += refill |
||||||
|
if state.BucketBytes > bl.burstLimit { |
||||||
|
state.BucketBytes = bl.burstLimit |
||||||
|
} |
||||||
|
state.LastUpdate = now |
||||||
|
} |
||||||
|
|
||||||
|
// Check if upload fits in bucket
|
||||||
|
if state.BucketBytes >= sizeBytes { |
||||||
|
state.BucketBytes -= sizeBytes |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// GetAvailable returns the currently available bytes for an identity.
|
||||||
|
func (bl *BandwidthLimiter) GetAvailable(identity string) int64 { |
||||||
|
bl.mu.Lock() |
||||||
|
defer bl.mu.Unlock() |
||||||
|
|
||||||
|
state, exists := bl.states[identity] |
||||||
|
if !exists { |
||||||
|
return bl.burstLimit // New users have full capacity
|
||||||
|
} |
||||||
|
|
||||||
|
// Calculate current level with refill
|
||||||
|
now := time.Now() |
||||||
|
elapsed := now.Sub(state.LastUpdate).Seconds() |
||||||
|
refill := int64(elapsed * bl.refillRate) |
||||||
|
available := state.BucketBytes + refill |
||||||
|
if available > bl.burstLimit { |
||||||
|
available = bl.burstLimit |
||||||
|
} |
||||||
|
|
||||||
|
return available |
||||||
|
} |
||||||
|
|
||||||
|
// GetTimeUntilAvailable returns how long until the given bytes will be available.
|
||||||
|
func (bl *BandwidthLimiter) GetTimeUntilAvailable(identity string, sizeBytes int64) time.Duration { |
||||||
|
available := bl.GetAvailable(identity) |
||||||
|
if available >= sizeBytes { |
||||||
|
return 0 |
||||||
|
} |
||||||
|
|
||||||
|
needed := sizeBytes - available |
||||||
|
seconds := float64(needed) / bl.refillRate |
||||||
|
return time.Duration(seconds * float64(time.Second)) |
||||||
|
} |
||||||
|
|
||||||
|
// Cleanup removes entries that have fully replenished (at burst limit).
|
||||||
|
func (bl *BandwidthLimiter) Cleanup() { |
||||||
|
bl.mu.Lock() |
||||||
|
defer bl.mu.Unlock() |
||||||
|
|
||||||
|
now := time.Now() |
||||||
|
for key, state := range bl.states { |
||||||
|
elapsed := now.Sub(state.LastUpdate).Seconds() |
||||||
|
refill := int64(elapsed * bl.refillRate) |
||||||
|
if state.BucketBytes+refill >= bl.burstLimit { |
||||||
|
delete(bl.states, key) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Stats returns the number of tracked identities.
|
||||||
|
func (bl *BandwidthLimiter) Stats() int { |
||||||
|
bl.mu.Lock() |
||||||
|
defer bl.mu.Unlock() |
||||||
|
return len(bl.states) |
||||||
|
} |
||||||
Loading…
Reference in new issue