Claude 5e2094f2ad
feat(media): local inference sidecar for Media Studio
FastAPI service in infra/media-worker implementing every
MediaInferenceProvider operation behind HTTP API v1, plus capabilities.
Bearer auth, MEDIA_ROOT realpath enforcement, strict request schemas,
cancellation on disconnect, GPU/CPU gates with 503 overflow, input
size/duration limits, lazy SHA256-pinned model downloads, fail-closed
redaction with atomic output, offline pytest suite with fake backends,
CPU/CUDA/ROCm Dockerfile and a license review of every model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138YqDj3ttfayRwuMKKrqVj
2026-10-03 18:19:47 +00:00

118 lines
3.6 KiB
Python

"""Cancellation on disconnect, the time limit, and GPU queue overflow."""
from __future__ import annotations
import asyncio
import threading
from pathlib import Path
import numpy as np
import pytest
from app import media_io
from app.errors import Busy, Cancelled
from app.runtime import Gate, run_job
from .conftest import AUTH
from .fakes import BlockingEmbedder, fake_backend
class FakeRequest:
def __init__(self, disconnect_after: int | None) -> None:
self.calls = 0
self.disconnect_after = disconnect_after
async def is_disconnected(self) -> bool:
self.calls += 1
return self.disconnect_after is not None and self.calls > self.disconnect_after
def _spin(seen: dict[str, bool]): # type: ignore[no-untyped-def]
def work(cancel: threading.Event) -> str:
for _ in range(400):
if cancel.wait(0.01):
seen["cancelled"] = True
raise Cancelled()
return "done"
return work
def test_disconnect_cancels_work() -> None:
seen: dict[str, bool] = {}
gate = Gate(1, 0)
with pytest.raises(Cancelled) as error:
asyncio.run(run_job(FakeRequest(disconnect_after=1), gate, _spin(seen), max_seconds=60))
assert error.value.reason == "client disconnected"
assert seen["cancelled"]
assert gate.inflight == 0
def test_time_limit_cancels_work() -> None:
seen: dict[str, bool] = {}
with pytest.raises(Cancelled) as error:
asyncio.run(run_job(FakeRequest(None), Gate(1, 0), _spin(seen), max_seconds=0.3))
assert error.value.reason == "exceeded MEDIA_MAX_SECONDS"
def test_completed_work_returns_result() -> None:
assert asyncio.run(run_job(FakeRequest(None), Gate(1, 0), lambda _c: "ok", max_seconds=5)) == "ok"
def test_gate_overflow_is_busy() -> None:
gate = Gate(1, 1)
gate.reserve()
gate.reserve()
with pytest.raises(Busy):
gate.reserve()
gate.release_reservation()
gate.reserve()
def test_cancelled_transform_leaves_no_output(media_root: Path) -> None:
cancel = threading.Event()
path = str(media_root / "testsrc.mp4")
info = media_io.probe(path, threading.Event())
out = media_root / "cancelled" / "out.mkv"
out.parent.mkdir(exist_ok=True)
def frame_fn(index: int, _t: int, frame: np.ndarray) -> np.ndarray:
if index == 5:
cancel.set()
return frame
with pytest.raises(Cancelled):
media_io.transform_video(path, str(out), info, cancel, frame_fn)
assert list(out.parent.iterdir()) == []
def test_http_busy_when_gpu_queue_is_full(make_client, media_root: Path) -> None:
blocking = BlockingEmbedder()
client = make_client(
backends={"embedFrames": fake_backend(blocking, "fake-embed")}, gpu_concurrency=1, queue_size=0
)
body = {"path": f"{media_root}/testsrc.mp4", "timesMs": [0]}
results: list[int] = []
first = threading.Thread(
target=lambda: results.append(client.post("/v1/embedFrames", headers=AUTH, json=body).status_code)
)
first.start()
assert blocking.entered.wait(20)
second = client.post("/v1/embedFrames", headers=AUTH, json=body)
assert second.status_code == 503
assert second.json()["error"] == {
"code": "busy",
"message": "inference queue is full",
"operation": "embedFrames",
}
# CPU-bound operations use their own gate and still run.
assert (
client.post(
"/v1/saliency", headers=AUTH, json={"path": f"{media_root}/spot.mp4", "timesMs": [0]}
).status_code
== 200
)
blocking.release.set()
first.join(20)
assert results == [200]