fix(plex): playback sessions per viewer, bounded in total; a fair share of subtitle passes

HLS sessions were keyed by the item, so a second account opening the same
film killed the first one's session; the displaced player reclaimed at its
playhead and evicted the other in turn. The registry is now keyed by
(viewer, rating_key), segment requests resolve by both, and the session
directory carries the viewer (the orphan sweep still recognises the older
shapes).

Only transcodes counted toward a cap, so remuxes — each a whole segmented
copy of the film on the scratch volume — were bounded by the library size.
Three bounds now apply after every start, each dropping the least recently
used: two sessions per account, the transcode cap, and a new admin knob,
plex_max_sessions (default 8), over every session. A superseded session
request returns at once, since it waits on the killed ffmpeg, so an account
cannot park more request workers than it may hold sessions.

The subtitle extractor's full-pass slots were global and minutes long each;
one account opening subtitles on film after film could hold them all. An
account may now hold three of the six, and still gets windows past that.
This commit is contained in:
2026-09-19 07:16:17 +02:00
parent 6f9ba428d5
commit dc4e8b9fcd
13 changed files with 278 additions and 102 deletions
+8 -8
View File
@@ -116,10 +116,10 @@ def test_the_periodic_pass_still_reaps_what_nobody_has_touched_for_the_idle_limi
def test_touching_a_session_keeps_it_through_the_short_rule(tmp_path, sessions):
s = _Session(tmp_path, "paused", finished=True, idle_s=90)
sessions["12345"] = s
stream.touch_session("12345")
sessions[(1, "12345")] = s
stream.touch_session(1, "12345")
assert stream.reap_idle() == 0
assert "12345" in sessions and s.dir.exists() and not s.proc.killed
assert (1, "12345") in sessions and s.dir.exists() and not s.proc.killed
# --- the two call sites, driven through the real routes (DB lane) -------------------------------
@@ -157,7 +157,7 @@ def test_a_paused_viewers_checkpoint_keeps_their_session_alive(db, monkeypatch,
db.add(user)
db.commit()
s = _Session(tmp_path, "paused", finished=True, idle_s=90)
sessions["paused1"] = s
sessions[(user.id, "paused1")] = s
try:
resp = _client(db, user).post(
f"/api/plex/item/{it.rating_key}/progress",
@@ -167,7 +167,7 @@ def test_a_paused_viewers_checkpoint_keeps_their_session_alive(db, monkeypatch,
app.dependency_overrides.clear()
assert resp.status_code == 200, resp.text
assert stream.reap_idle() == 0, "the checkpoint did not say the viewer is still there"
assert "paused1" in sessions and not s.proc.killed
assert (user.id, "paused1") in sessions and not s.proc.killed
def test_starting_a_session_leaves_another_viewers_paused_one_alone(
@@ -183,9 +183,9 @@ def test_starting_a_session_leaves_another_viewers_paused_one_alone(
db.add(user)
db.commit()
paused = _Session(tmp_path, "paused", finished=True, idle_s=90)
sessions["someone-else"] = paused
sessions[(999, "someone-else")] = paused
def fake_start(db_, item, start, audio=None, aoff=0.0, multi=False, transcode=False):
def fake_start(db_, item, user_id, start, audio=None, aoff=0.0, multi=False, transcode=False):
return SimpleNamespace(
id="sid",
media_start_s=start,
@@ -201,4 +201,4 @@ def test_starting_a_session_leaves_another_viewers_paused_one_alone(
finally:
app.dependency_overrides.clear()
assert resp.status_code == 200, resp.text
assert "someone-else" in sessions and not paused.proc.killed
assert (999, "someone-else") in sessions and not paused.proc.killed
+1 -1
View File
@@ -68,7 +68,7 @@ def test_a_first_ever_finish_inserts_and_counts_once(db, make_user):
def test_the_finish_supersedes_a_resume_push_still_in_flight(db, as_user, make_user, monkeypatch):
user = make_user()
item = _item(db)
monkeypatch.setattr(playback.plex_stream, "touch_session", lambda key: None)
monkeypatch.setattr(playback.plex_stream, "touch_session", lambda uid, key: None)
earlier = datetime.now(timezone.utc) - timedelta(seconds=5)
# A resume push was scheduled a moment ago and has not reached Plex yet.
assert playback._should_push_resume(user.id, item.id, earlier, final=True)
+32 -18
View File
@@ -38,6 +38,9 @@ class _Proc:
return 0
VIEWER = 41 # the id of the account the test client is signed in as
class _Session:
def __init__(self, session_id: str, directory, running: bool = False, audio_langs=None):
self.id = session_id
@@ -61,7 +64,7 @@ def sessions(monkeypatch):
@pytest.fixture
def client(monkeypatch):
monkeypatch.setattr(state, "_configured_cache", True)
user = User(email="hls@example.com")
user = User(id=VIEWER, email="hls@example.com")
app.dependency_overrides[current_user] = lambda: user
try:
yield TestClient(app)
@@ -74,12 +77,23 @@ def test_a_segment_is_served_from_the_session_the_url_names(client, tmp_path, se
d.mkdir()
(d / "seg_11.ts").write_bytes(b"") # its predecessor, so seg_12 is the next one
(d / "seg_12.ts").write_bytes(b"mine")
sessions["m1"] = _Session("sid-a", d)
sessions[(VIEWER, "m1")] = _Session("sid-a", d)
r = client.get("/api/plex/stream/m1/hls/sid-a/seg_12.ts")
assert r.status_code == 200, r.text
assert r.content == b"mine"
def test_another_viewers_session_of_the_same_item_is_not_this_viewers(client, tmp_path, sessions):
"""Sessions are per viewer: a second account playing the same film has its own, and knowing its
id does not make it yours — resolution is by (viewer, item) before the id is compared."""
d = tmp_path / "m1_u7_0_0_+0.00"
d.mkdir()
(d / "index.m3u8").write_bytes(b"#EXTM3U")
sessions[(7, "m1")] = _Session("sid-a", d)
r = client.get("/api/plex/stream/m1/hls/sid-a/index.m3u8")
assert r.status_code == 404, r.text
def test_a_segment_of_a_replaced_session_is_a_404_not_the_new_sessions_bytes(
client, tmp_path, sessions
):
@@ -89,7 +103,7 @@ def test_a_segment_of_a_replaced_session_is_a_404_not_the_new_sessions_bytes(
theirs.mkdir()
(theirs / "seg_11.ts").write_bytes(b"")
(theirs / "seg_12.ts").write_bytes(b"theirs")
sessions["m1"] = _Session("sid-b", theirs)
sessions[(VIEWER, "m1")] = _Session("sid-b", theirs)
r = client.get("/api/plex/stream/m1/hls/sid-a/seg_12.ts")
assert r.status_code == 404, r.text
assert b"theirs" not in r.content
@@ -105,7 +119,7 @@ def test_a_playlist_with_no_live_session_is_a_404_not_a_silent_restart_at_zero(
monkeypatch.setattr(
plex_stream,
"start_session",
lambda *a, **k: started.append(a[2] if len(a) > 2 else 0.0),
lambda *a, **k: started.append(a[3] if len(a) > 3 else 0.0),
)
r = client.get("/api/plex/stream/m1/hls/sid-gone/index.m3u8")
assert r.status_code == 404, r.text
@@ -122,7 +136,7 @@ def test_a_segment_that_will_never_arrive_fails_at_once(client, tmp_path, sessio
d = tmp_path / "m1_0_0_+0.00"
d.mkdir()
(d / "seg_0.ts").write_bytes(b"x") # the session is producing; seg_99999 is simply not next
sessions["m1"] = _Session("sid-a", d, running=True)
sessions[(VIEWER, "m1")] = _Session("sid-a", d, running=True)
started = time.monotonic()
r = client.get("/api/plex/stream/m1/hls/sid-a/seg_99999.ts")
elapsed = time.monotonic() - started
@@ -150,7 +164,7 @@ def test_a_multi_audio_segment_beyond_the_region_fails_at_once_too(
d.mkdir()
(d / "seg_0_0.ts").write_bytes(b"x")
(d / "seg_a0_0.ts").write_bytes(b"x")
sessions["m1"] = _Session("sid-a", d, running=True, audio_langs=["hu", "en"])
sessions[(VIEWER, "m1")] = _Session("sid-a", d, running=True, audio_langs=["hu", "en"])
started = time.monotonic()
r = client.get(f"/api/plex/stream/m1/hls/sid-a/{name}")
elapsed = time.monotonic() - started
@@ -166,7 +180,7 @@ def test_a_multi_audio_segment_whose_predecessor_exists_is_served(client, tmp_pa
d.mkdir()
(d / "seg_a0_0.ts").write_bytes(b"x")
(d / "seg_a0_1.ts").write_bytes(b"audio")
sessions["m1"] = _Session("sid-a", d, running=True, audio_langs=["hu", "en"])
sessions[(VIEWER, "m1")] = _Session("sid-a", d, running=True, audio_langs=["hu", "en"])
r = client.get("/api/plex/stream/m1/hls/sid-a/seg_a0_1.ts")
assert r.status_code == 200, r.text
assert r.content == b"audio"
@@ -179,7 +193,7 @@ def test_a_name_the_session_cannot_produce_fails_at_once(client, tmp_path, sessi
its playlists and its segments and nothing else, so any other name is answerable now."""
d = tmp_path / "m1_0_0_+0.00"
d.mkdir()
sessions["m1"] = _Session("sid-a", d, running=True)
sessions[(VIEWER, "m1")] = _Session("sid-a", d, running=True)
started = time.monotonic()
r = client.get("/api/plex/stream/m1/hls/sid-a/nope.m3u8")
elapsed = time.monotonic() - started
@@ -194,7 +208,7 @@ def test_an_absurd_segment_index_is_a_404_not_a_500(client, tmp_path, sessions,
subtitle routes were bounded for; the answer is the same 404 any other unproducible name gets."""
d = tmp_path / "m1_0_0_+0.00"
d.mkdir()
sessions["m1"] = _Session("sid-a", d, running=True)
sessions[(VIEWER, "m1")] = _Session("sid-a", d, running=True)
r = client.get(f"/api/plex/stream/m1/hls/sid-a/seg_{'9' * digits}.ts")
assert r.status_code == 404, r.text
@@ -209,7 +223,7 @@ def test_a_session_torn_down_during_the_read_is_a_404_not_a_500(
d = tmp_path / "m1_0_0_+0.00"
d.mkdir()
(d / "seg_11.ts").write_bytes(b"")
sessions["m1"] = _Session("sid-a", d)
sessions[(VIEWER, "m1")] = _Session("sid-a", d)
# The file existed when the wait checked, and is gone by the time the route reads it.
monkeypatch.setattr(plex_stream, "wait_for", lambda *a, **k: True)
r = client.get("/api/plex/stream/m1/hls/sid-a/seg_12.ts")
@@ -224,7 +238,7 @@ def test_a_dead_producer_is_not_waited_out(client, tmp_path, sessions):
d = tmp_path / "m1_0_0_+0.00"
d.mkdir()
(d / "seg_5.ts").write_bytes(b"x")
sessions["m1"] = _Session("sid-a", d, running=False)
sessions[(VIEWER, "m1")] = _Session("sid-a", d, running=False)
started = time.monotonic()
r = client.get("/api/plex/stream/m1/hls/sid-a/seg_6.ts")
elapsed = time.monotonic() - started
@@ -294,10 +308,10 @@ def test_a_name_only_the_other_argv_branch_writes_is_refused_at_once(tmp_path, c
30.0 s and `seg_a0_0.ts` 20.1 s to answer 404 (deep-review round 7)."""
single = tmp_path / "m1_0_0_+0.00"
single.mkdir()
sessions["m1"] = _Session("sid-a", single, running=True)
sessions[(VIEWER, "m1")] = _Session("sid-a", single, running=True)
multi = tmp_path / "m2_0_multi_+0.00"
multi.mkdir()
sessions["m2"] = _Session("sid-b", multi, running=True, audio_langs=["hu", "en"])
sessions[(VIEWER, "m2")] = _Session("sid-b", multi, running=True, audio_langs=["hu", "en"])
# Each of these belongs to the other shape (or to no shape at all) and must be refused without
# a wait — the assertion is the ELAPSED time as much as the status.
@@ -315,8 +329,8 @@ def test_a_name_only_the_other_argv_branch_writes_is_refused_at_once(tmp_path, c
)
# And the control: the names each session CAN write are still waited for, not refused.
assert plex_stream.artifact_may_arrive(sessions["m1"], "index.m3u8")
assert plex_stream.artifact_may_arrive(sessions["m1"], "seg_0.ts")
assert plex_stream.artifact_may_arrive(sessions["m2"], "master.m3u8")
assert plex_stream.artifact_may_arrive(sessions["m2"], "stream_a1.m3u8")
assert plex_stream.artifact_may_arrive(sessions["m2"], "seg_a1_0.ts")
assert plex_stream.artifact_may_arrive(sessions[(VIEWER, "m1")], "index.m3u8")
assert plex_stream.artifact_may_arrive(sessions[(VIEWER, "m1")], "seg_0.ts")
assert plex_stream.artifact_may_arrive(sessions[(VIEWER, "m2")], "master.m3u8")
assert plex_stream.artifact_may_arrive(sessions[(VIEWER, "m2")], "stream_a1.m3u8")
assert plex_stream.artifact_may_arrive(sessions[(VIEWER, "m2")], "seg_a1_0.ts")
+1 -1
View File
@@ -55,7 +55,7 @@ def _post(db, user, monkeypatch, rating_key, *, captured):
monkeypatch.setattr(plex_stream, "reap_idle", lambda: None)
monkeypatch.setattr(plex_paths, "local_media_path", lambda db_, fp: "/local/a.mkv")
def fake_start(db_, it, start, audio, aoff, multi, transcode=False):
def fake_start(db_, it, user_id, start, audio, aoff, multi, transcode=False):
captured["transcode"] = transcode
return SimpleNamespace(
id="sid",
+78 -17
View File
@@ -108,7 +108,7 @@ class TestSweepOrphans:
def test_a_live_sessions_directory_is_never_deleted(self, hls_root):
# Registered session, but with an OLD directory — only the live check can save it.
d = _session_dir(hls_root, "12345_0_multi_+0.00", age_s=3600)
stream._sessions["k"] = _FakeSession(d)
stream._sessions[(1, "k")] = _FakeSession(d)
assert stream.sweep_orphans(older_than=_cutoff(hls_root)) == 0
assert d.exists()
@@ -179,10 +179,20 @@ class _CapturingProc:
def __init__(self, *args, **kwargs):
_CapturingProc.calls.append((args, kwargs))
self.killed = False
def poll(self):
return None
def terminate(self):
self.killed = True
def wait(self, timeout=None):
return 0
def kill(self):
self.killed = True
def _venc(cmd):
"""The video codec ffmpeg was told to use (the value after the first `-c:v`)."""
@@ -495,7 +505,7 @@ class TestColdStartWarmup:
return True
monkeypatch.setattr(stream, "wait_for", spy)
s = stream.start_session(db=None, item=_FakeItem(), start_s=0.0)
s = stream.start_session(db=None, user_id=7, item=_FakeItem(), start_s=0.0)
assert s is not None
# seg_0 (unconditional now) + the warm-up run up to _WARMUP_SEGMENTS.
assert waited == [f"seg_{i}.ts" for i in range(stream._WARMUP_SEGMENTS)]
@@ -508,7 +518,7 @@ class TestColdStartWarmup:
return path.name != "seg_1.ts" # seg_1 never appears → warm-up gives up there
monkeypatch.setattr(stream, "wait_for", spy)
s = stream.start_session(db=None, item=_FakeItem(), start_s=0.0)
s = stream.start_session(db=None, user_id=7, item=_FakeItem(), start_s=0.0)
assert s is not None
assert waited == ["seg_0.ts", "seg_1.ts"] # never asked for seg_2
@@ -522,7 +532,7 @@ class TestColdStartWarmup:
return False
monkeypatch.setattr(stream, "wait_for", spy)
s = stream.start_session(db=None, item=_FakeItem(), start_s=0.0)
s = stream.start_session(db=None, user_id=7, item=_FakeItem(), start_s=0.0)
assert s is not None
assert waited == ["seg_0.ts"]
@@ -548,6 +558,7 @@ class TestTranscodeStartSession:
"plex_transcode_crf": 18,
"plex_transcode_max_height": 480,
"plex_max_transcodes": 4,
"plex_max_sessions": 8,
}[k],
)
monkeypatch.setattr(
@@ -573,7 +584,7 @@ class TestTranscodeStartSession:
stream, "_select_encode", lambda db, item, transcode, src: (False, False)
)
s = stream.start_session(
db=None, item=_FakeItem(codec_video="mpeg4"), start_s=0.0, transcode=True
db=None, user_id=7, item=_FakeItem(codec_video="mpeg4"), start_s=0.0, transcode=True
)
assert s is not None and s.transcode is True and s.hw is False
(cmd, *_), kwargs = _CapturingProc.calls[0]
@@ -590,7 +601,7 @@ class TestTranscodeStartSession:
stream, "_select_encode", lambda db, item, transcode, src: (True, False)
)
s = stream.start_session(
db=None, item=_FakeItem(codec_video="hevc"), start_s=0.0, transcode=True
db=None, user_id=7, item=_FakeItem(codec_video="hevc"), start_s=0.0, transcode=True
)
assert s is not None and s.transcode is True and s.hw is True and s.tonemap is False
assert len(_CapturingProc.calls) == 1 # first segment arrived — no HW→SW fallback respawn
@@ -609,7 +620,7 @@ class TestTranscodeStartSession:
# tonemap=True on the session, still on the GPU (hw=True) and never niced.
monkeypatch.setattr(stream, "_select_encode", lambda db, item, transcode, src: (True, True))
s = stream.start_session(
db=None, item=_FakeItem(codec_video="hevc"), start_s=0.0, transcode=True
db=None, user_id=7, item=_FakeItem(codec_video="hevc"), start_s=0.0, transcode=True
)
assert s is not None and s.transcode is True and s.hw is True and s.tonemap is True
(cmd, *_), kwargs = _CapturingProc.calls[0]
@@ -635,7 +646,7 @@ class TestTranscodeStartSession:
monkeypatch.setattr(stream, "wait_for", flaky_wait)
s = stream.start_session(
db=None, item=_FakeItem(codec_video="hevc"), start_s=0.0, transcode=True
db=None, user_id=7, item=_FakeItem(codec_video="hevc"), start_s=0.0, transcode=True
)
assert (
s is not None and s.transcode is True and s.hw is False
@@ -666,7 +677,7 @@ class TestTranscodeStartSession:
monkeypatch.setattr(stream, "wait_for", flaky_wait)
s = stream.start_session(
db=None, item=_FakeItem(codec_video="hevc"), start_s=0.0, transcode=True
db=None, user_id=7, item=_FakeItem(codec_video="hevc"), start_s=0.0, transcode=True
)
assert s is not None and s.transcode is True and s.hw is False and s.tonemap is False
assert len(_CapturingProc.calls) == 2 # the HW tonemap attempt, then the SW respawn
@@ -699,17 +710,17 @@ class TestTranscodeStartSession:
def wait_then_supersede(path, timeout=20.0, proc=None):
# Simulate the concurrent replacement happening during our out-of-lock wait, then report the
# (now-killed) HW proc produced nothing.
stream._sessions["12345"] = newer
stream._sessions[(7, "12345")] = newer
return False
monkeypatch.setattr(stream, "wait_for", wait_then_supersede)
s = stream.start_session(
db=None, item=_FakeItem(codec_video="hevc"), start_s=0.0, transcode=True
db=None, user_id=7, item=_FakeItem(codec_video="hevc"), start_s=0.0, transcode=True
)
# No SW respawn — only the initial HW _spawn ran; the newer session was left intact.
assert len(_CapturingProc.calls) == 1
assert s is newer # yielded the currently-registered session, did not clobber it
assert stream._sessions["12345"] is newer
assert stream._sessions[(7, "12345")] is newer
def test_a_cap_culled_hw_session_does_not_respawn_and_is_not_reported_missing(
self, capturing_env, monkeypatch
@@ -727,16 +738,16 @@ class TestTranscodeStartSession:
def wait_then_cull(path, timeout=20.0, proc=None):
captured["s"] = stream._sessions[
"12345"
(7, "12345")
] # the HW session the initial _spawn just registered
del stream._sessions[
"12345"
(7, "12345")
] # simulate _enforce_cap dropping the LRU under cap pressure
return False
monkeypatch.setattr(stream, "wait_for", wait_then_cull)
s = stream.start_session(
db=None, item=_FakeItem(codec_video="hevc"), start_s=0.0, transcode=True
db=None, user_id=7, item=_FakeItem(codec_video="hevc"), start_s=0.0, transcode=True
)
assert (
len(_CapturingProc.calls) == 1
@@ -752,7 +763,9 @@ class TestTranscodeStartSession:
monkeypatch.setattr(
stream, "_select_encode", lambda db, item, transcode, src: (False, False)
)
s = stream.start_session(db=None, item=_FakeItem(), start_s=0.0) # transcode defaults False
s = stream.start_session(
db=None, user_id=7, item=_FakeItem(), start_s=0.0
) # transcode defaults False
assert s is not None and s.transcode is False
(cmd, *_), kwargs = _CapturingProc.calls[0]
assert _venc(cmd) == "copy"
@@ -896,7 +909,7 @@ class TestAudioLoudnorm:
)
if multi:
monkeypatch.setattr(stream, "_probe_audio_langs", lambda src: ["hun", "eng"])
s = stream.start_session(db=None, item=_FakeItem(), start_s=0.0, multi=multi)
s = stream.start_session(db=None, user_id=7, item=_FakeItem(), start_s=0.0, multi=multi)
assert s is not None
(cmd, *_), _ = _CapturingProc.calls[0]
return s, cmd
@@ -945,3 +958,51 @@ class TestAudioLoudnorm:
# sweep no longer recognises is one that never gets cleaned up.
s, _ = self._spawn(monkeypatch, on=on)
assert _SESSION_DIR_RE.match(s.dir.name)
class TestSessionsPerViewer:
"""HLS sessions were keyed by the ITEM, so a second account opening the same film killed the
first one's session; each then reclaimed and evicted the other. And only transcodes counted
toward a cap, so remuxes — each a whole segmented copy of the film on disk — were unbounded."""
capturing_env = TestTranscodeStartSession.capturing_env
@staticmethod
def _start(monkeypatch, user_id, rating_key="12345"):
monkeypatch.setattr(
stream, "_select_encode", lambda db, item, transcode, src: (False, False)
)
return stream.start_session(
db=None, user_id=user_id, item=_FakeItem(rating_key=rating_key), start_s=0.0
)
def test_two_viewers_of_one_film_each_keep_their_own_session(self, capturing_env, monkeypatch):
monkeypatch.setattr(stream, "_sessions", {})
a = self._start(monkeypatch, user_id=1)
b = self._start(monkeypatch, user_id=2)
assert stream._sessions == {(1, "12345"): a, (2, "12345"): b}
assert not a.proc.killed
assert a.dir != b.dir and _SESSION_DIR_RE.match(a.dir.name)
def test_one_account_holds_at_most_two_sessions(self, capturing_env, monkeypatch):
monkeypatch.setattr(stream, "_sessions", {})
first = self._start(monkeypatch, user_id=1, rating_key="1")
first.last_access -= 60 # the least recently used of the three
self._start(monkeypatch, user_id=1, rating_key="2")
self._start(monkeypatch, user_id=1, rating_key="3")
assert sorted(stream._sessions) == [(1, "2"), (1, "3")]
assert first.proc.killed
def test_remux_sessions_count_toward_the_overall_ceiling(self, capturing_env, monkeypatch):
monkeypatch.setattr(stream, "_sessions", {})
monkeypatch.setattr(
stream.sysconfig,
"get_int",
lambda db, k: {"plex_max_transcodes": 4, "plex_max_sessions": 3}[k],
)
oldest = self._start(monkeypatch, user_id=1)
oldest.last_access -= 60
for uid in (2, 3, 4):
self._start(monkeypatch, user_id=uid)
assert len(stream._sessions) == 3 and (1, "12345") not in stream._sessions
assert oldest.proc.killed and not oldest.transcode
+23 -2
View File
@@ -690,8 +690,8 @@ def test_a_scrubbing_viewer_cannot_queue_a_window_per_stop(monkeypatch):
def test_nothing_more_is_queued_while_the_extractor_is_saturated(monkeypatch):
pool = _manual_pool(monkeypatch)
for rk in range(20):
subtitles.ensure(str(rk), 0, at=0.0)
for rk in range(20): # twenty viewers, so no one account's share is what stops the passes
subtitles.ensure(str(rk), 0, at=0.0, viewer=rk)
windows = [a for _, a in pool.jobs if a[3] is not None]
assert len(windows) == subtitles._MAX_WINDOWS_INFLIGHT
assert len(pool.jobs) - len(windows) == subtitles._MAX_FULL_INFLIGHT
@@ -971,6 +971,27 @@ def test_a_window_is_not_refused_while_full_passes_hold_their_slots(monkeypatch)
assert full.jobs == [], "past its bound no full pass is queued"
def test_one_account_cannot_hold_every_full_pass_slot(monkeypatch):
"""The full-pass slots are global and minutes long each: one account opening subtitles on film
after film held them all, and nobody else's track ever completed. Past its share an account
still gets windows — what makes a track watchable now — and the next account still gets a pass."""
win, full = _ManualPool(), _ManualPool()
monkeypatch.setattr(subtitles, "_WIN_POOL", win)
monkeypatch.setattr(subtitles, "_FULL_POOL", full)
monkeypatch.setattr(subtitles, "_extract_and_publish", lambda *a: None)
for rk in range(subtitles._MAX_FULL_PER_USER + 1):
subtitles.ensure(str(1000 + rk), 1, at=600.0, viewer=7)
assert len(full.jobs) == subtitles._MAX_FULL_PER_USER
assert len(win.jobs) == subtitles._MAX_FULL_PER_USER + 1, (
"the account over its share still gets windows"
)
subtitles.ensure("2000", 1, at=600.0, viewer=8)
assert len(full.jobs) == subtitles._MAX_FULL_PER_USER + 1, "another account is not shut out"
full.run_all() # the account's passes end: its share is free again
subtitles.ensure("3000", 1, at=600.0, viewer=7)
assert any(a[1] == "3000" and a[3] is None for _, a in full.jobs)
def test_a_poll_after_shutdown_queues_nothing(monkeypatch):
pool = _manual_pool(monkeypatch)
subtitles.shutdown()