fix(plex): a segment name ffmpeg never spells is refused; teardown no longer holds the registry lock

The artifact guard parsed the sequence with int(), so a zero-padded or non-ASCII-digit name read
as a segment the session had reached and parked the sync route for the full 20 s wait. It now
also requires the canonical spelling segment_name() produces.

Stopping, replacing, culling or reaping a session waited up to 3 s for ffmpeg and deleted a whole
segmented film under _lock, which every segment request and checkpoint takes. Teardown now signals
the process and moves its directory aside under the lock, and waits/deletes after it. A new session
on the same path still waits for the dying ffmpeg first (it writes by path), and the orphan sweep
recognises a moved-aside directory left by a crash.
This commit is contained in:
2026-10-28 18:48:21 +01:00
parent 4084dd2f92
commit e96a562f14
7 changed files with 289 additions and 24 deletions
+26 -1
View File
@@ -198,6 +198,31 @@ def test_a_name_the_session_cannot_produce_fails_at_once(client, tmp_path, sessi
assert elapsed < 2.0, f"waited {elapsed:.1f}s for a playlist this session never writes"
@pytest.mark.parametrize(
("name", "audio_langs"),
[
("seg_00.ts", None), # `int()` reads it as segment 0, which needs no predecessor
("seg_007.ts", None), # …as segment 7, whose predecessor seg_6.ts is on disk
("seg_٧.ts", None), # an Arabic-Indic 7: `\d` and `int()` both take it
("seg_a0_07.ts", ["hu", "en"]),
("seg_0_00.ts", ["hu", "en"]),
],
)
def test_a_segment_name_ffmpeg_never_spells_is_refused(tmp_path, name, audio_langs):
"""ffmpeg writes `seg_%d.ts`: no padding, ASCII digits. A name that only PARSES to a sequence the
session has reached is still a file that will never appear, and saying yes to it parked the
request for the full 20 s wait — the parking this guard exists to end."""
d = tmp_path / "m1_0_0_+0.00"
d.mkdir()
for v in plex_stream.session_variants(audio_langs) or (None,):
(d / plex_stream.segment_name(6, v)).write_bytes(b"x")
sess = _Session("sid-a", d, running=True, audio_langs=audio_langs)
assert not plex_stream.artifact_may_arrive(sess, name)
# The canonical spelling of the same next segment is still accepted.
v = (plex_stream.session_variants(audio_langs) or (None,))[0]
assert plex_stream.artifact_may_arrive(sess, plex_stream.segment_name(7, v))
@pytest.mark.parametrize("digits", [100, 5000])
def test_an_absurd_segment_index_is_a_404_not_a_500(client, tmp_path, sessions, digits):
"""`int()` on an unbounded digit run raises ValueError past 4300 digits, and only OSError was
@@ -213,7 +238,7 @@ def test_an_absurd_segment_index_is_a_404_not_a_500(client, tmp_path, sessions,
def test_a_session_torn_down_during_the_read_is_a_404_not_a_500(
client, monkeypatch, tmp_path, sessions
):
"""`_kill` rmtrees the whole directory and nothing holds `_lock` across the wait and the read,
"""Teardown moves the whole directory aside and nothing holds `_lock` across the wait and the read,
so the file can vanish in between — on every seek-restart, since `loadSession` awaits the new
session before destroying the old hls instance. The player's recovery is keyed on the 404
STATUS, so a 500 here skipped the reclaim and ended playback with "couldn't play"."""