You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

131 lines
3.8 KiB

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)
}