Client progress reports could keep a zero-byte phantom session alive and, worse, make it outrank a genuinely-serving stream when the enforcer picked over-cap victims. `streammonitor.LiveLocalSessions` substituted `Session.LastActivityAt` for a zero `LastServedAt`, and `LastActivityAt` is advanced by `UpdateProgress` and by the realtime WebSocket hello/ack/result handlers. Since `streamenforcer.selectVictims` keeps the `limit` most-recently-served streams, a progress-only phantom sorted ahead of a real stream and the real one was trimmed instead. Reaping had the same root cause: `sessionIsInactiveLocked` keyed idleness on `LastActivityAt`, so a client that kept pinging held a session open forever. Per decision A5 (Option C), client progress is now UI metadata only and never feeds enforcement or reaping: - `LiveLocalSessions` projects `LastServedAt` verbatim, emitting an empty timestamp when the session has never served, so it sorts as the stalest over-cap victim. - `sessionIsInactiveLocked` measures idleness from `LastServedAt`, falling back only to `StartedAt`. - A configurable never-served window (`DefaultUnservedSessionGrace`, 2m, via `SetUnservedSessionGrace`) keeps a legitimately slow start from being reaped before its first byte, without granting a phantom unbounded life. It is a separate knob rather than a hardcoded floor so it cannot silently override `SetLivenessGracePeriods`. In-flight transports remain exempt, so direct-play and remux long pours and per-segment HLS serves are unaffected. Paused sessions with an open realtime/WebSocket connection are exempt from reaping. That preserves the issue #243 fix (reaping a paused transcode froze clients) while staying within Option C: an open, ping-checked connection is server-observed, unlike a client's reported progress, and the session still consumes one of the user's cap slots. Part 1 of 3 for the Batch 4 liveness/replica work. Part of #305
81 lines
2.4 KiB
Go
81 lines
2.4 KiB
Go
package playback
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestAddServedBytesUnknownDoesNotCreateSession(t *testing.T) {
|
|
m := NewSessionManager(0, 0)
|
|
if err := m.AddServedBytes("missing", 100); !errors.Is(err, ErrSessionNotFound) {
|
|
t.Fatalf("AddServedBytes error=%v, want ErrSessionNotFound", err)
|
|
}
|
|
if len(m.AllSessions()) != 0 {
|
|
t.Fatal("AddServedBytes recreated an unknown session")
|
|
}
|
|
}
|
|
|
|
func TestLastServedAtOnlyAdvancesForServerObservedEvents(t *testing.T) {
|
|
m := NewSessionManager(0, 0)
|
|
s, err := m.StartSession(1, "p", 1, PlayDirect, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.UpdateProgress(s.ID, 10, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
afterProgress, _ := m.GetSession(s.ID)
|
|
if !afterProgress.LastServedAt.IsZero() {
|
|
t.Fatalf("UpdateProgress advanced LastServedAt: %v", afterProgress.LastServedAt)
|
|
}
|
|
if err := m.AddServedBytes(s.ID, 7); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
afterBytes, _ := m.GetSession(s.ID)
|
|
if afterBytes.LastServedAt.IsZero() || afterBytes.BytesServed != 7 {
|
|
t.Fatalf("AddServedBytes did not update served state: %+v", afterBytes)
|
|
}
|
|
first := afterBytes.LastServedAt
|
|
time.Sleep(time.Millisecond)
|
|
if err := m.BeginTransport(s.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
afterBegin, _ := m.GetSession(s.ID)
|
|
if !afterBegin.LastServedAt.After(first) {
|
|
t.Fatal("BeginTransport did not advance LastServedAt")
|
|
}
|
|
}
|
|
|
|
func TestTranscodeLivenessGraceAndPausedPrecedence(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
method PlayMethod
|
|
paused bool
|
|
idle time.Duration
|
|
wantReaped bool
|
|
}{
|
|
{name: "transcode five minutes retained", method: PlayTranscode, idle: 5 * time.Minute},
|
|
{name: "transcode fifteen minutes reaped", method: PlayTranscode, idle: 15 * time.Minute, wantReaped: true},
|
|
{name: "paused transcode keeps thirty minutes", method: PlayTranscode, paused: true, idle: 15 * time.Minute},
|
|
{name: "direct keeps active grace", method: PlayDirect, idle: time.Minute, wantReaped: true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
m := NewSessionManager(0, 0)
|
|
s, err := m.StartSession(1, "p", 1, tt.method, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m.mu.Lock()
|
|
m.sessions[s.ID].IsPaused = tt.paused
|
|
m.sessions[s.ID].LastServedAt = time.Now().Add(-tt.idle)
|
|
m.mu.Unlock()
|
|
reaped := m.CleanStale()
|
|
if (len(reaped) > 0) != tt.wantReaped {
|
|
t.Fatalf("reaped=%d, wantReaped=%v", len(reaped), tt.wantReaped)
|
|
}
|
|
})
|
|
}
|
|
}
|