test(stream): fix data race in MediaStreamer transcoding cap tests

The three It blocks that build a tight-cap streamer each spawned a fresh
transcoding cache without waiting for its background initialization. The
init goroutine reads conf.Server.CacheFolder, which races against
SnapshotConfig's pointer-swap restore (Server = &restored) fired by
DeferCleanup at the end of the spec. CI tripped the race under
-shuffle=on -race; locally it reproduced about 10% of the time.

Wait for tightCache.Available() before constructing the streamer, mirroring
the outer BeforeEach. For the slot-saturation spec, swap in a blocking
io.Pipe-backed mock ffmpeg so the cache's background copyAndClose can't
drain the source and release the slot — the previous behavior happened to
work only because the cache wasn't yet available and the no-cache path was
exercised.
This commit is contained in:
Deluan 2026-05-28 00:07:49 -03:00 committed by Rob Emery
parent 7f7b91f785
commit abe6d59037

View File

@ -64,11 +64,19 @@ var _ = Describe("MediaStreamer", func() {
Expect(s.Duration()).To(Equal(float32(257.0)))
})
It("rejects transcode requests beyond MaxConcurrent with ErrTooManyTranscodes", func() {
// Rebuild the streamer with a tight cap. The first request will hold the
// ffmpeg reader open (we don't read/close it), saturating the single slot.
// Use an ffmpeg whose Read blocks indefinitely so the cache's
// background copy can't drain the source and release the slot —
// keeping the single transcode slot pinned for this test.
pr, pw := io.Pipe()
DeferCleanup(func() { _ = pw.Close() })
blockingFFmpeg := tests.NewMockFFmpeg("")
blockingFFmpeg.Reader = pr
conf.Server.Transcoding.MaxConcurrent = 1
conf.Server.Transcoding.MaxConcurrentPerUser = 0
tightStreamer := stream.NewMediaStreamer(ds, ffmpeg, stream.NewTranscodingCache())
tightCache := stream.NewTranscodingCache()
Eventually(func() bool { return tightCache.Available(context.TODO()) }).Should(BeTrue())
tightStreamer := stream.NewMediaStreamer(ds, blockingFFmpeg, tightCache)
userCtx := request.WithUsername(ctx, "alice")
s1, err := tightStreamer.NewStream(userCtx, mf, stream.Request{Format: "mp3", BitRate: 64})
@ -83,7 +91,9 @@ var _ = Describe("MediaStreamer", func() {
It("releases the slot once the stream is closed", func() {
conf.Server.Transcoding.MaxConcurrent = 1
conf.Server.Transcoding.MaxConcurrentPerUser = 0
tightStreamer := stream.NewMediaStreamer(ds, ffmpeg, stream.NewTranscodingCache())
tightCache := stream.NewTranscodingCache()
Eventually(func() bool { return tightCache.Available(context.TODO()) }).Should(BeTrue())
tightStreamer := stream.NewMediaStreamer(ds, ffmpeg, tightCache)
userCtx := request.WithUsername(ctx, "alice")
s1, err := tightStreamer.NewStream(userCtx, mf, stream.Request{Format: "mp3", BitRate: 64})
@ -101,7 +111,9 @@ var _ = Describe("MediaStreamer", func() {
It("does not consume a slot for raw streams", func() {
conf.Server.Transcoding.MaxConcurrent = 1
conf.Server.Transcoding.MaxConcurrentPerUser = 0
tightStreamer := stream.NewMediaStreamer(ds, ffmpeg, stream.NewTranscodingCache())
tightCache := stream.NewTranscodingCache()
Eventually(func() bool { return tightCache.Available(context.TODO()) }).Should(BeTrue())
tightStreamer := stream.NewMediaStreamer(ds, ffmpeg, tightCache)
userCtx := request.WithUsername(ctx, "alice")
// First, saturate the single transcode slot.