205 lines
5.3 KiB
Go
205 lines
5.3 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
type Metrics struct {
|
|
ActiveSessions atomic.Int64
|
|
AdmittedSessions atomic.Uint64
|
|
AdmissionRejects atomic.Uint64
|
|
Reconnects atomic.Uint64
|
|
DrainTransitions atomic.Uint64
|
|
MediaDrops atomic.Uint64
|
|
MediaPackets atomic.Uint64
|
|
MediaBytes atomic.Uint64
|
|
QueueDelayNanos atomic.Uint64
|
|
ProcessingDelayNanos atomic.Uint64
|
|
ProcessingSamples atomic.Uint64
|
|
PacingDelayNanos atomic.Uint64
|
|
ProviderErrors atomic.Uint64
|
|
InputRejected atomic.Uint64
|
|
ControlRTTNanos atomic.Uint64
|
|
ControlJitterNanos atomic.Uint64
|
|
ControlLossPPM atomic.Uint64
|
|
PendingReliable atomic.Uint64
|
|
ProviderState atomic.Uint64
|
|
}
|
|
|
|
type MetricsSnapshot struct {
|
|
ActiveSessions int64
|
|
AdmittedSessions uint64
|
|
AdmissionRejects uint64
|
|
Reconnects uint64
|
|
DrainTransitions uint64
|
|
MediaDrops uint64
|
|
MediaPackets uint64
|
|
MediaBytes uint64
|
|
QueueDelayNanos uint64
|
|
ProcessingDelayNanos uint64
|
|
ProcessingSamples uint64
|
|
PacingDelayNanos uint64
|
|
ProviderErrors uint64
|
|
InputRejected uint64
|
|
ControlRTTNanos uint64
|
|
ControlJitterNanos uint64
|
|
ControlLossPPM uint64
|
|
PendingReliable uint64
|
|
ProviderState uint64
|
|
}
|
|
|
|
func (m *Metrics) Snapshot() MetricsSnapshot {
|
|
return MetricsSnapshot{
|
|
ActiveSessions: m.ActiveSessions.Load(),
|
|
AdmittedSessions: m.AdmittedSessions.Load(),
|
|
AdmissionRejects: m.AdmissionRejects.Load(),
|
|
Reconnects: m.Reconnects.Load(),
|
|
DrainTransitions: m.DrainTransitions.Load(),
|
|
MediaDrops: m.MediaDrops.Load(),
|
|
MediaPackets: m.MediaPackets.Load(),
|
|
MediaBytes: m.MediaBytes.Load(),
|
|
QueueDelayNanos: m.QueueDelayNanos.Load(),
|
|
ProcessingDelayNanos: m.ProcessingDelayNanos.Load(),
|
|
ProcessingSamples: m.ProcessingSamples.Load(),
|
|
PacingDelayNanos: m.PacingDelayNanos.Load(),
|
|
ProviderErrors: m.ProviderErrors.Load(),
|
|
InputRejected: m.InputRejected.Load(),
|
|
ControlRTTNanos: m.ControlRTTNanos.Load(),
|
|
ControlJitterNanos: m.ControlJitterNanos.Load(),
|
|
ControlLossPPM: m.ControlLossPPM.Load(),
|
|
PendingReliable: m.PendingReliable.Load(),
|
|
ProviderState: m.ProviderState.Load(),
|
|
}
|
|
}
|
|
|
|
func (m *Metrics) observeProviderTelemetry(telemetry ProviderTelemetry) {
|
|
if m == nil {
|
|
return
|
|
}
|
|
m.ControlRTTNanos.Store(uint64(telemetry.ControlRTT))
|
|
m.ControlJitterNanos.Store(uint64(telemetry.ControlJitter))
|
|
m.PendingReliable.Store(telemetry.PendingReliable)
|
|
if telemetry.ReliableSent == 0 {
|
|
m.ControlLossPPM.Store(0)
|
|
} else {
|
|
m.ControlLossPPM.Store(telemetry.ReliableRetransmits * 1_000_000 / telemetry.ReliableSent)
|
|
}
|
|
}
|
|
|
|
func (m *Metrics) observeProviderState(state string) {
|
|
if m == nil {
|
|
return
|
|
}
|
|
switch state {
|
|
case ProviderStateStarting:
|
|
m.ProviderState.Store(1)
|
|
case ProviderStateReady:
|
|
m.ProviderState.Store(2)
|
|
case ProviderStateDisconnected:
|
|
m.ProviderState.Store(3)
|
|
case ProviderStateTerminating:
|
|
m.ProviderState.Store(4)
|
|
case ProviderStateTerminated:
|
|
m.ProviderState.Store(5)
|
|
case ProviderStateCleanup:
|
|
m.ProviderState.Store(6)
|
|
case ProviderStateFailed:
|
|
m.ProviderState.Store(7)
|
|
default:
|
|
m.ProviderState.Store(0)
|
|
}
|
|
}
|
|
|
|
// fairPacer is the gateway's one shared, equal-tier media scheduler. Each
|
|
// session can hold only its existing bounded provider media channel while it
|
|
// waits for the next reservation, so a slow client cannot grow a global queue.
|
|
type fairPacer struct {
|
|
mu sync.Mutex
|
|
bytesPerSecond int64
|
|
flows map[string]fairPacerFlow
|
|
reservations atomic.Uint64
|
|
}
|
|
|
|
type fairPacerFlow struct {
|
|
next time.Time
|
|
lastSeen time.Time
|
|
}
|
|
|
|
const fairPacerMaximumCatchup = 5 * time.Millisecond
|
|
|
|
func newFairPacer(kbps int64) *fairPacer {
|
|
pacer := &fairPacer{flows: make(map[string]fairPacerFlow)}
|
|
pacer.setKbps(kbps)
|
|
return pacer
|
|
}
|
|
|
|
func (p *fairPacer) setKbps(kbps int64) {
|
|
if p == nil {
|
|
return
|
|
}
|
|
p.mu.Lock()
|
|
if kbps > 0 {
|
|
p.bytesPerSecond = kbps * 1000 / 8
|
|
} else {
|
|
p.bytesPerSecond = 0
|
|
}
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
func (p *fairPacer) remove(flow string) {
|
|
if p == nil || flow == "" {
|
|
return
|
|
}
|
|
p.mu.Lock()
|
|
delete(p.flows, flow)
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
func (p *fairPacer) reserveAt(now time.Time, flow string, bytes int) time.Time {
|
|
if p == nil || flow == "" || bytes < 1 {
|
|
return now
|
|
}
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.bytesPerSecond < 1 {
|
|
return now
|
|
}
|
|
for key, state := range p.flows {
|
|
if now.Sub(state.lastSeen) > time.Second {
|
|
delete(p.flows, key)
|
|
}
|
|
}
|
|
state := p.flows[flow]
|
|
state.lastSeen = now
|
|
p.flows[flow] = state
|
|
base := state.next
|
|
if base.IsZero() {
|
|
base = now
|
|
} else if lag := now.Sub(base); lag > fairPacerMaximumCatchup {
|
|
base = now.Add(-fairPacerMaximumCatchup)
|
|
}
|
|
numerator := int64(bytes) * int64(len(p.flows)) * int64(time.Second)
|
|
delay := time.Duration((numerator + p.bytesPerSecond - 1) / p.bytesPerSecond)
|
|
state.next = base.Add(delay)
|
|
p.flows[flow] = state
|
|
return state.next
|
|
}
|
|
|
|
func (p *fairPacer) wait(ctx context.Context, flow string, bytes int) error {
|
|
target := p.reserveAt(time.Now(), flow, bytes)
|
|
p.reservations.Add(1)
|
|
if delay := time.Until(target); delay > 0 {
|
|
timer := time.NewTimer(delay)
|
|
defer timer.Stop()
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-timer.C:
|
|
}
|
|
}
|
|
return nil
|
|
}
|