package client import ( "sync" "time" ) // minShaperWait floors the dispatcher's sleep so floating-point dust in the // token arithmetic cannot spin it. const minShaperWait = time.Millisecond // Shaper caps the aggregate rate at which the client writes DATA to the hub and // divides that budget across streams. // // The credit windows of PROTOCOL.md ยง7.3 bound how many bytes may be *in flight* // per stream; they say nothing about bytes per *second*. That is the gap this // fills. On a residential uplink one player loading chunks will otherwise // saturate the line and push every other player's keepalive past its timeout. // // Two mechanisms are layered: // // - A token bucket sets the long-run rate and the size of the burst that may // be spent after an idle period. // - Start-time fair queueing decides who spends those tokens. A global virtual // clock advances with each grant; every stream remembers the virtual time at // which its last request finished. A request is stamped // max(share.vfinish, vclock) and the lowest stamp is served first, so a // stream that keeps sending pushes its own stamp further out and yields to // quieter streams. The clamp to vclock is what keeps bursts cheap: a stream // returning from idle is pulled back to the head of the clock, so it cannot // hoard credit while it was idle, but it is not punished for the idleness // either. One stream alone gets the whole rate. // // A nil *Shaper means "no limit"; every method short-circuits, so call sites do // not branch. type Shaper struct { rate float64 // bytes per second burst float64 // token bucket capacity, bytes chunk int // how much a caller should request at a time mu sync.Mutex tokens float64 last time.Time vclock float64 // virtual time, in bytes of service granted waiting []*shaperReq // unordered; the dispatcher scans for the lowest vstart wake chan struct{} // cap 1, non-blocking: nudges the dispatcher done chan struct{} once sync.Once } // shaperShare is one stream's position in the fair queue. It lives on the // Stream and dies with it; a fresh share starts at zero and is clamped up to // the current virtual clock on its first request. type shaperShare struct{ vfinish float64 } // shaperReq is one pending Acquire. granted and membership in Shaper.waiting // are both guarded by Shaper.mu. type shaperReq struct { n int vstart float64 grant chan struct{} granted bool } // NewShaper builds a shaper for the given rate. A non-positive rate returns nil, // which every method treats as "unlimited". func NewShaper(bytesPerSec int64) *Shaper { if bytesPerSec <= 0 { return nil } rate := float64(bytesPerSec) burst := rate * ShaperBurstSeconds // The floor is a correctness constraint, not a preference: a request larger // than the bucket could never be afforded and would park forever. if burst < MinShaperBurst { burst = MinShaperBurst } if burst > MaxShaperBurst { burst = MaxShaperBurst } chunk := int(rate * ShaperSliceSeconds) if chunk < MinShaperChunk { chunk = MinShaperChunk } if chunk > DataChunkSize { chunk = DataChunkSize } sh := &Shaper{ rate: rate, burst: burst, chunk: chunk, tokens: burst, last: time.Now(), wake: make(chan struct{}, 1), done: make(chan struct{}), } go sh.dispatch() return sh } // chunkSize is how many bytes a sender should offer per request. It is sized to // ShaperSliceSeconds of transmission so no stream holds the link for long before // the scheduler can switch: at 1 Mbps a full 32 KiB chunk takes ~256 ms, which is // enough dead air to drag other players towards a keepalive timeout. func (sh *Shaper) chunkSize() int { if sh == nil { return DataChunkSize } return sh.chunk } // Acquire blocks until n bytes of bandwidth budget are available for the stream // owning share. It returns false only when cancel fires first, in which case // nothing was charged. // // cancel is the stream's done channel: a stream torn down while parked here must // not keep a goroutine (and its Stream) alive waiting for tokens it will never // use. func (sh *Shaper) Acquire(share *shaperShare, n int, cancel <-chan struct{}) bool { if sh == nil || n <= 0 { return true } req := &shaperReq{n: n, grant: make(chan struct{})} sh.mu.Lock() // Stamp the request and reserve this stream's slot in virtual time up front, // so a stream cannot queue many requests at the same cheap stamp. req.vstart = share.vfinish if req.vstart < sh.vclock { req.vstart = sh.vclock } share.vfinish = req.vstart + float64(n) sh.waiting = append(sh.waiting, req) sh.mu.Unlock() sh.nudge() select { case <-req.grant: return true case <-sh.done: // Shaping stopped: let live traffic through rather than stalling it. sh.mu.Lock() sh.removeLocked(req) sh.mu.Unlock() return true case <-cancel: sh.mu.Lock() granted := req.granted if !granted { sh.removeLocked(req) } sh.mu.Unlock() return granted } } // Stop shuts the dispatcher down and releases everyone parked in Acquire. func (sh *Shaper) Stop() { if sh == nil { return } sh.once.Do(func() { close(sh.done) }) } // dispatch is the single goroutine that hands out tokens. It sleeps exactly as // long as the next waiter needs rather than polling on a fixed tick, so an idle // shaper costs nothing. func (sh *Shaper) dispatch() { for { wait := sh.grantReady() var tick <-chan time.Time var timer *time.Timer if wait > 0 { timer = time.NewTimer(wait) tick = timer.C } select { case <-tick: case <-sh.wake: case <-sh.done: if timer != nil { timer.Stop() } return } if timer != nil { timer.Stop() } } } // grantReady refills the bucket and grants every waiter it can afford, lowest // virtual start time first. It returns how long until the next waiter becomes // affordable, or 0 when nothing is pending. func (sh *Shaper) grantReady() time.Duration { sh.mu.Lock() defer sh.mu.Unlock() now := time.Now() if elapsed := now.Sub(sh.last); elapsed > 0 { sh.tokens += sh.rate * elapsed.Seconds() if sh.tokens > sh.burst { sh.tokens = sh.burst } sh.last = now } for { req := sh.headLocked() if req == nil { return 0 } // Callers stay under chunkSize, which NewShaper keeps below the bucket. // Should a future caller not, wait for a full bucket rather than for a // token count that can never be reached, and let the balance go negative: // the debt is repaid by the next refill, so the long-run rate still holds. need := min(float64(req.n), sh.burst) if need > sh.tokens { wait := time.Duration((need - sh.tokens) / sh.rate * float64(time.Second)) if wait < minShaperWait { wait = minShaperWait } return wait } sh.tokens -= float64(req.n) // The clock follows the request being served, never runs ahead of it. if req.vstart > sh.vclock { sh.vclock = req.vstart } req.granted = true sh.removeLocked(req) close(req.grant) } } // headLocked returns the pending request with the lowest virtual start time. // A linear scan is deliberate: the queue holds at most one entry per live // stream (tens, not thousands), so a heap would cost more in complexity than it // saves in comparisons. func (sh *Shaper) headLocked() *shaperReq { var best *shaperReq for _, w := range sh.waiting { if best == nil || w.vstart < best.vstart { best = w } } return best } func (sh *Shaper) removeLocked(req *shaperReq) { for i, w := range sh.waiting { if w == req { sh.waiting = append(sh.waiting[:i], sh.waiting[i+1:]...) return } } } func (sh *Shaper) nudge() { select { case sh.wake <- struct{}{}: default: } }