fix(playback): make session liveness server-observed
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
This commit is contained in:
@@ -13,11 +13,10 @@
|
||||
// (BeginTransport/EndTransport around every byte pour) — no client progress
|
||||
// required to stay visible.
|
||||
//
|
||||
// TIMING is a secondary signal. On the edge, LastServedAt is purely byte-observed.
|
||||
// In integrated mode LastServedAt is mapped from SessionManager.LastActivityAt,
|
||||
// which client progress reports also advance; that is acceptable because it is
|
||||
// used only to order over-cap victims (selectVictims), never to decide whether a
|
||||
// stream exists or is counted. See internal/nodesessions for record production.
|
||||
// TIMING is server-observed on every path. LastServedAt advances only when the
|
||||
// server begins, ends, or writes a media transport. A session that has not
|
||||
// served media projects an empty LastServedAt and sorts as the stalest over-cap
|
||||
// victim. See internal/nodesessions for record production.
|
||||
package streammonitor
|
||||
|
||||
import (
|
||||
@@ -315,9 +314,9 @@ func LiveLocalSessions(sm *playback.SessionManager, nodeName string) []nodesessi
|
||||
live := sm.AllSessions()
|
||||
out := make([]nodesessions.SessionInfo, 0, len(live))
|
||||
for _, s := range live {
|
||||
lastServedAt := s.LastServedAt
|
||||
if lastServedAt.IsZero() {
|
||||
lastServedAt = s.LastActivityAt
|
||||
lastServedAt := ""
|
||||
if !s.LastServedAt.IsZero() {
|
||||
lastServedAt = s.LastServedAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
out = append(out, nodesessions.SessionInfo{
|
||||
SessionID: s.ID,
|
||||
@@ -333,7 +332,7 @@ func LiveLocalSessions(sm *playback.SessionManager, nodeName string) []nodesessi
|
||||
Resolution: s.TargetResolution,
|
||||
HWAccel: s.TranscodeHWAccel,
|
||||
StartedAt: s.StartedAt.UTC().Format(time.RFC3339),
|
||||
LastServedAt: lastServedAt.UTC().Format(time.RFC3339),
|
||||
LastServedAt: lastServedAt,
|
||||
BytesServed: s.BytesServed,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -363,6 +363,16 @@ func TestLiveLocalSessionsMapping(t *testing.T) {
|
||||
t.Fatalf("StartSessionWithContext: %v", err)
|
||||
}
|
||||
session.ClientIP = "192.0.2.10"
|
||||
if err := sm.BeginTransport(session.ID); err != nil {
|
||||
t.Fatalf("BeginTransport: %v", err)
|
||||
}
|
||||
if err := sm.EndTransport(session.ID); err != nil {
|
||||
t.Fatalf("EndTransport: %v", err)
|
||||
}
|
||||
session, err = sm.GetSession(session.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSession: %v", err)
|
||||
}
|
||||
got := LiveLocalSessions(sm, "local")
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("sessions = %+v", got)
|
||||
@@ -375,7 +385,26 @@ func TestLiveLocalSessionsMapping(t *testing.T) {
|
||||
info.ClientIP != "192.0.2.10" {
|
||||
t.Fatalf("mapped session = %+v", info)
|
||||
}
|
||||
if info.LastServedAt != session.LastActivityAt.UTC().Format(time.RFC3339) {
|
||||
t.Fatalf("LastServedAt = %q, want LastActivityAt fallback", info.LastServedAt)
|
||||
if info.LastServedAt != session.LastServedAt.UTC().Format(time.RFC3339) {
|
||||
t.Fatalf("LastServedAt = %q, want server-observed timestamp", info.LastServedAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiveLocalSessionsDoesNotProjectClientActivityAsLastServed(t *testing.T) {
|
||||
sm := playback.NewSessionManager(0, 0)
|
||||
session, err := sm.StartSession(42, "profile-1", 9, playback.PlayDirect, false)
|
||||
if err != nil {
|
||||
t.Fatalf("StartSession: %v", err)
|
||||
}
|
||||
if err := sm.UpdateProgress(session.ID, 12, false); err != nil {
|
||||
t.Fatalf("UpdateProgress: %v", err)
|
||||
}
|
||||
|
||||
got := LiveLocalSessions(sm, "local")
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("sessions = %+v", got)
|
||||
}
|
||||
if got[0].LastServedAt != "" {
|
||||
t.Fatalf("LastServedAt = %q, want empty for a never-served session", got[0].LastServedAt)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user