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.
38 lines
1.2 KiB
Go
38 lines
1.2 KiB
Go
package nodesessions
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
func TestTranscodeIdleTTLAgreesAcrossCountSnapshotAndRefresh(t *testing.T) {
|
|
rdb := redis.NewClient(&redis.Options{Addr: "127.0.0.1:1", DialTimeout: 10 * time.Millisecond})
|
|
t.Cleanup(func() { _ = rdb.Close() })
|
|
tr := NewTracker(rdb, "http://node", "node", "proxy")
|
|
idle := time.Now().Add(-90 * time.Second)
|
|
tr.touched[sessionTypeTranscode] = idle
|
|
tr.records[sessionTypeTranscode] = SessionInfo{SessionID: sessionTypeTranscode, Type: sessionTypeTranscode}
|
|
tr.touched["direct"] = idle
|
|
tr.records["direct"] = SessionInfo{SessionID: "direct", Type: "direct_play"}
|
|
|
|
if got := tr.ActiveCount(); got != 1 {
|
|
t.Fatalf("ActiveCount=%d, want only transcode", got)
|
|
}
|
|
snapshot := tr.Snapshot()
|
|
if len(snapshot) != 1 || snapshot[0].SessionID != sessionTypeTranscode {
|
|
t.Fatalf("Snapshot=%+v, want only transcode", snapshot)
|
|
}
|
|
|
|
tr.refreshAll(context.Background())
|
|
tr.mu.Lock()
|
|
_, transcodeRetained := tr.records[sessionTypeTranscode]
|
|
_, directRetained := tr.records["direct"]
|
|
tr.mu.Unlock()
|
|
if !transcodeRetained || directRetained {
|
|
t.Fatalf("refresh records: transcode=%v direct=%v, want true/false", transcodeRetained, directRetained)
|
|
}
|
|
}
|