Files
silo-server/internal/playback/segment_pruner_test.go
T

230 lines
7.1 KiB
Go

package playback
import (
"io"
"os"
"path/filepath"
"testing"
"time"
)
func TestReportSegmentDownloadedPrunesOnlyExpiredBackBuffer(t *testing.T) {
dir := t.TempDir()
old := time.Now().Add(-time.Minute)
for segment := 0; segment <= 15; segment++ {
writePrunerTestFile(t, filepath.Join(dir, segmentFilename(segment, TranscodeOpts{})), []byte("segment"), old)
}
writePrunerTestFile(t, filepath.Join(dir, "init.mp4"), []byte("init"), old)
writePrunerTestFile(t, filepath.Join(dir, "stream.m3u8"), []byte("manifest"), old)
writePrunerTestFile(t, filepath.Join(dir, "seg_00004.ts.tmp"), []byte("temporary"), old)
writePrunerTestFile(t, filepath.Join(dir, "notes.txt"), []byte("other"), old)
// A newly-created file below the floor may belong to a replacement ffmpeg
// process and must survive this pass.
writePrunerTestFile(t, filepath.Join(dir, segmentFilename(4, TranscodeOpts{})), []byte("fresh"), time.Now())
session := &TranscodeSession{
opts: TranscodeOpts{
SessionID: "prune-test",
SegmentDuration: 2,
SegmentRetentionSeconds: 10,
},
outputDir: dir,
lastPruneFloor: -1,
lastRequestedSegment: 0,
}
session.ReportSegmentDownloaded(15)
waitForPrunerFileMissing(t, filepath.Join(dir, segmentFilename(9, TranscodeOpts{})))
t.Cleanup(func() {
session.mu.Lock()
session.segmentGeneration++
session.segmentPruneRunning = false
session.mu.Unlock()
})
for _, segment := range []int{0, 1, 2, 4, 10, 11, 12, 13, 14, 15} {
assertPrunerFileExists(t, filepath.Join(dir, segmentFilename(segment, TranscodeOpts{})))
}
for _, segment := range []int{3, 5, 6, 7, 8, 9} {
assertPrunerFileMissing(t, filepath.Join(dir, segmentFilename(segment, TranscodeOpts{})))
}
for _, name := range []string{"init.mp4", "stream.m3u8", "seg_00004.ts.tmp", "notes.txt"} {
assertPrunerFileExists(t, filepath.Join(dir, name))
}
}
func TestFreshSegmentIsRetriedAfterGuardExpires(t *testing.T) {
dir := t.TempDir()
opts := TranscodeOpts{
SessionID: "retry-test",
SegmentDuration: 2,
SegmentRetentionSeconds: 10,
}
guard := 2*time.Duration(opts.SegmentDuration)*time.Second + 30*time.Second
path := filepath.Join(dir, segmentFilename(3, opts))
writePrunerTestFile(t, path, []byte("segment"), time.Now().Add(-guard+time.Second))
session := &TranscodeSession{
opts: opts,
outputDir: dir,
lastPruneFloor: -1,
lastRequestedSegment: 0,
}
session.ReportSegmentDownloaded(9)
waitForPrunerFileMissing(t, path)
waitForPrunePass(t, session, 4)
}
func TestPruningContinuesAcrossBoundedBatches(t *testing.T) {
dir := t.TempDir()
old := time.Now().Add(-time.Minute)
opts := TranscodeOpts{
SessionID: "batch-test",
SegmentDuration: 2,
SegmentRetentionSeconds: 10,
}
for segment := 0; segment <= 600; segment++ {
writePrunerTestFile(t, filepath.Join(dir, segmentFilename(segment, opts)), []byte("segment"), old)
}
session := &TranscodeSession{
opts: opts,
outputDir: dir,
lastPruneFloor: -1,
lastRequestedSegment: 0,
}
session.ReportSegmentDownloaded(600)
waitForPrunerFileMissing(t, filepath.Join(dir, segmentFilename(594, opts)))
waitForPrunePass(t, session, 595)
for _, segment := range []int{0, 1, 2, 595, 600} {
assertPrunerFileExists(t, filepath.Join(dir, segmentFilename(segment, opts)))
}
}
func TestReportSegmentDownloadedDisabledKeepsSegments(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, segmentFilename(3, TranscodeOpts{}))
writePrunerTestFile(t, path, []byte("segment"), time.Now().Add(-time.Minute))
session := &TranscodeSession{
opts: TranscodeOpts{SegmentDuration: 2},
outputDir: dir,
lastPruneFloor: -1,
lastRequestedSegment: 0,
}
session.ReportSegmentDownloaded(500)
time.Sleep(20 * time.Millisecond)
assertPrunerFileExists(t, path)
}
func TestOldGenerationDownloadDoesNotAdvanceRestartedSession(t *testing.T) {
session := &TranscodeSession{
opts: TranscodeOpts{SegmentRetentionSeconds: 600},
lastRequestedSegment: 100,
}
oldGeneration := session.SegmentGeneration()
session.mu.Lock()
session.segmentGeneration++
session.lastRequestedSegment = 20
session.mu.Unlock()
session.ReportSegmentDownloadedForGeneration(101, oldGeneration)
if got := session.LastRequestedSegment(); got != 20 {
t.Fatalf("last requested segment = %d, want restarted position 20", got)
}
}
func TestCloseInvalidatesInFlightSegmentGeneration(t *testing.T) {
session := &TranscodeSession{
opts: TranscodeOpts{SegmentRetentionSeconds: 600},
outputDir: t.TempDir(),
lastRequestedSegment: 20,
}
downloadGeneration := session.SegmentGeneration()
if err := session.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
session.ReportSegmentDownloadedForGeneration(100, downloadGeneration)
if got := session.LastRequestedSegment(); got != 20 {
t.Fatalf("last requested segment = %d, want closed-session position 20", got)
}
}
func TestOpenSegmentDescriptorSurvivesUnlink(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "seg_00001.ts")
want := []byte("complete segment")
writePrunerTestFile(t, path, want, time.Now())
session := &TranscodeSession{outputDir: dir}
segment, _, err := session.OpenSegment("seg_00001.ts")
if err != nil {
t.Fatalf("OpenSegment: %v", err)
}
defer func() { _ = segment.Close() }()
if err := os.Remove(path); err != nil {
t.Fatalf("remove opened segment: %v", err)
}
got, err := io.ReadAll(segment)
if err != nil {
t.Fatalf("read opened segment: %v", err)
}
if string(got) != string(want) {
t.Fatalf("opened segment = %q, want %q", got, want)
}
}
func writePrunerTestFile(t *testing.T, path string, contents []byte, modTime time.Time) {
t.Helper()
if err := os.WriteFile(path, contents, 0o600); err != nil {
t.Fatalf("write %s: %v", filepath.Base(path), err)
}
if err := os.Chtimes(path, modTime, modTime); err != nil {
t.Fatalf("set times for %s: %v", filepath.Base(path), err)
}
}
func waitForPrunePass(t *testing.T, session *TranscodeSession, wantFloor int) {
t.Helper()
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
session.mu.Lock()
running := session.segmentPruneRunning
floor := session.lastPruneFloor
session.mu.Unlock()
if !running && floor >= wantFloor {
return
}
time.Sleep(5 * time.Millisecond)
}
t.Fatal("segment prune pass did not finish")
}
func waitForPrunerFileMissing(t *testing.T, path string) {
t.Helper()
deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return
}
time.Sleep(5 * time.Millisecond)
}
t.Fatalf("expected %s to be removed", filepath.Base(path))
}
func assertPrunerFileExists(t *testing.T, path string) {
t.Helper()
if _, err := os.Stat(path); err != nil {
t.Fatalf("expected %s to exist: %v", filepath.Base(path), err)
}
}
func assertPrunerFileMissing(t *testing.T, path string) {
t.Helper()
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Fatalf("expected %s to be removed, stat error = %v", filepath.Base(path), err)
}
}