From 4742aa685129cf790d3f7510238919ffde15a949 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Thu, 17 Sep 2026 17:23:30 +0200 Subject: fix(node): size the subtitle budget to the file, and extract each track once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verifying the feature across a library turned up two faults, one of which broke it outright. **The extraction budget was a constant and the cost is not.** Extracting demuxes the whole container, so the file sets the price: measured at 9.8 s per GB on a library held on an external disk — 36 s for a 3.9 GB title, 71 s for a 7.3 GB one. Against a flat 60 s that worked on most of a library and failed on the big films, which to whoever is watching one is indistinguishable from a broken feature. `Avatar.The.Way.of.Water` timed out every time. The budget is now three times the measured rate per gigabyte, floored at the old 60 s and capped so no container can pin a transcode slot indefinitely. The client's own timeout follows it: the node always answers, so that one is a backstop against a silent peer rather than a deadline for the work. **Two clicks ran two extractions.** The cache is consulted on the way in, so a second request arriving while the first was still running missed it and ran its own — seen in the log as two identical extractions of one 4.3 GB file overlapping, each holding a transcode slot and reading the file end to end. Latecomers now wait on the answer the first is producing. The in-flight entry is registered *before* the first await, not after. The first version registered it after the probe, two concurrent requests both got past the check while neither had registered, and the test for it failed — which is the only reason this note can be written from the right side. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UGY17EPph5LsLzePPXhUVc --- .../tests/test_stream_subtitle_tracks.py | 55 +++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) (limited to 'packages/meshbay-node/tests/test_stream_subtitle_tracks.py') diff --git a/packages/meshbay-node/tests/test_stream_subtitle_tracks.py b/packages/meshbay-node/tests/test_stream_subtitle_tracks.py index 9a09edb..d3bcc8b 100644 --- a/packages/meshbay-node/tests/test_stream_subtitle_tracks.py +++ b/packages/meshbay-node/tests/test_stream_subtitle_tracks.py @@ -38,7 +38,11 @@ from meshbay_common.crypto import generate_gek from meshbay_common.webcrypto import chunk_key_aes, decrypt_chunk_aes from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.media_probe import TEXT_SUBTITLE_CODECS -from meshbay_node.transport.webrtc_server import WebRTCPeerSession, _probe_video +from meshbay_node.transport.webrtc_server import ( + WebRTCPeerSession, + _probe_video, + _subtitle_timeout_for, +) from conftest import needs_subprocess, one_root @@ -356,3 +360,52 @@ async def test_the_result_is_fetched_through_the_ordinary_chunk_path(tmp_path): plain = decrypt_chunk_aes(key, chunk["nonce"], chunk["ct"]) assert plain.decode("utf-8").startswith("WEBVTT") assert _CUE_WORD[2] in plain.decode("utf-8") + + +def test_the_budget_grows_with_the_file_not_with_the_subtitle(): + """Extraction demuxes the whole container, so the file sets the cost. + + Measured on a library held on an external disk: 9.8 s per GB — 36 s for a + 3.9 GB title and 71 s for a 7.3 GB one. A flat 60 s therefore worked on + most of a library and failed on the big films, which is indistinguishable + from a broken feature to whoever is watching one. The allowance is three + times the measured rate, so a slower disk still finishes. + """ + assert _subtitle_timeout_for(500_000_000) == 60 # small file, the floor + assert _subtitle_timeout_for(7_310_000_000) > 71 * 2 # the film that timed out + assert _subtitle_timeout_for(3_900_000_000) > 36 * 2 + # Bounded: a pathological container must not pin a transcode slot for ever. + assert _subtitle_timeout_for(500_000_000_000) == 900 + # Monotonic, or a bigger file could be given less time than a smaller one. + budgets = [_subtitle_timeout_for(int(gb * 1e9)) for gb in (1, 4, 8, 20, 100)] + assert budgets == sorted(budgets) + + +@pytest.mark.asyncio +async def test_two_requests_for_one_track_extract_once(tmp_path): + """Two clicks seconds apart used to run two whole extractions. + + The cache is consulted on the way in, so the second request missed it + while the first was still running: seen in the log as two identical + extractions of one 4.3 GB file overlapping, each holding a transcode slot + and reading the file end to end. The latecomer waits for the answer the + first is already producing, and both are answered. + """ + clip = tmp_path / "clip.mp4" + _make_subtitled_clip(clip) + gek = generate_gek() + session, file_id = _session(clip, gek) + + import asyncio + await asyncio.gather( + session._do_subtitle_request({"file_id": file_id, "track": 1}), + session._do_subtitle_request({"file_id": file_id, "track": 1}), + ) + + replies = [m for m in session.sent if m.get("type") == "subtitle_resp"] + assert len(replies) == 2, f"both callers must be answered: {session.sent}" + assert replies[0]["hash"] == replies[1]["hash"] + assert session._ctx["media_cache"].puts == 1, ( + "the second request ran its own extraction instead of joining the first") + assert not session._ctx["_subtitle_inflight"], ( + "the in-flight entry outlived the extraction and would block the next one") -- cgit v1.2.3