Files
memby/server/internal/credits/load.go
T

85 lines
2.7 KiB
Go
Raw Normal View History

2026-08-15 09:23:26 +12:00
package credits
import (
"context"
"sync"
"time"
)
// Whether the server is too busy to be reading media speculatively.
//
// Deliberately not a telemetry subsystem. The gateway already knows the only thing that
// actually matters here — how many televisions are playing something right now — because
// every one of them reports playback started, progress and stopped. A scan competes with
// those streams for the same disk and the same Emby, so the count is a better predictor of
// harm than a CPU figure would be, and it costs nothing to keep.
const (
// busyPlaybacks is how many concurrent streams make speculative scanning unwelcome. Two
// televisions is an ordinary evening in a household and the NAS copes; a third is the
// point at which adding ranged reads of a fourth file stops being free.
busyPlaybacks = 2
// playbackStale is how long a playback is believed without a progress report. Reports
// arrive every ten seconds, so a minute of silence means the television went away
// without saying so — a crash, a power cut, a network drop. Without this the gauge would
// latch busy for ever on one lost stop report and speculative scanning would never run
// again until the container restarted.
playbackStale = 90 * time.Second
)
// PlaybackLoad counts what is playing, from the reports televisions already send.
type PlaybackLoad struct {
mu sync.Mutex
active map[string]time.Time
}
func NewPlaybackLoad() *PlaybackLoad {
return &PlaybackLoad{active: map[string]time.Time{}}
}
// Playing records that a stream is alive. Called from playback started and from each
// progress report, which is what makes the staleness sweep work: a live stream keeps
// refreshing its own timestamp.
func (l *PlaybackLoad) Playing(sessionKey string) {
if l == nil || sessionKey == "" {
return
}
l.mu.Lock()
l.active[sessionKey] = time.Now()
l.mu.Unlock()
}
// Stopped records that a stream ended.
func (l *PlaybackLoad) Stopped(sessionKey string) {
if l == nil || sessionKey == "" {
return
}
l.mu.Lock()
delete(l.active, sessionKey)
l.mu.Unlock()
}
// Busy is the one question the scheduler asks.
func (l *PlaybackLoad) Busy(context.Context) bool {
return l.Count() >= busyPlaybacks
}
// Count is the live stream count, sweeping anything that stopped reporting. The sweep is
// here rather than on a ticker because this is the only reader, and a background goroutine
// to expire at most a handful of map entries would cost more than it tidies.
func (l *PlaybackLoad) Count() int {
if l == nil {
return 0
}
cutoff := time.Now().Add(-playbackStale)
l.mu.Lock()
defer l.mu.Unlock()
for key, seen := range l.active {
if seen.Before(cutoff) {
delete(l.active, key)
}
}
return len(l.active)
}