Files
silo-server/internal/playback/session_served_test.go
T
CoffeeKnyte 4b2031fdf8 feat(playback): account served bytes and hold transcode liveness server-side
Integrated deployments recorded no served bytes at all: BytesServed was only
ever advanced by the edge writer, so a single-node install showed
bytes_served: 0 for every stream. Worse, integrated LastServedAt was mapped
from Session.LastActivityAt, which UpdateProgress advances — letting a client
influence which of its own streams the over-cap enforcer trims first.

- internal/playback: Session gains BytesServed and a distinct LastServedAt.
  LastServedAt advances only from server-observed events (AddServedBytes,
  BeginTransport, EndTransport) and never from a client progress report.
  LastActivityAt keeps its exact prior meaning and reaping semantics.
- internal/playback/metered_writer: one shared SessionMeteredWriter for every
  integrated pour. It forwards io.ReaderFrom so the kernel sendfile path
  survives, guards the fallback copy against re-entering ReadFrom, and
  implements Unwrap() so the revocation cut's SetWriteDeadline still reaches
  the socket. Both properties have regressed on this branch before (GAP-3,
  GAP-9) and are now pinned by a chain test.
- Wired at every integrated pour: native direct-play/remux and transcode
  segment, jellycompat direct/remux and HLS segment. Each site defers the tail
  flush; the wrapper alone loses the final partial chunk.
- Close VERIFY-4 (buffer-ahead evasion): an unpaused transcode gets a bounded
  10m grace measured from the server-observed clock. This covers local,
  cleanly-completed and offloaded transcodes uniformly, unlike an ffmpeg
  liveness probe, which sees only local processes and reports false once a
  copy-mode encode finishes ahead of playback. Paused sessions keep their
  load-bearing 30m grace.
- internal/nodesessions: the same idle window one layer out — transcode
  records idle out at 180s instead of 60s, applied through a single helper
  used by ActiveCount, Snapshot and refreshAll so the node's count and status
  view cannot disagree.

Part of the stream monitoring & kill-switch epic.
2026-07-29 15:16:59 +00:00

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].LastActivityAt = 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)
}
})
}
}