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:
CoffeeKnyte
2026-07-31 00:03:44 +00:00
parent 4288c7c5c6
commit fc65dcaf16
8 changed files with 272 additions and 47 deletions
+8 -9
View File
@@ -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,
})
}