fix(replay): recover failed archives and release deleted sessions

This commit is contained in:
DCCONSTRUCTIONS
2026-07-29 01:55:23 +03:00
parent 2a97cf28c0
commit 1eba7c82ba
8 changed files with 217 additions and 7 deletions
+87
View File
@@ -22,6 +22,7 @@ from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC
from k1link.sessions import (
MaterializedRecording,
RecordedMediaInspector,
RecordedMediaManifest,
ReplayCommand,
SessionIntegrityError,
SessionNotFoundError,
@@ -362,8 +363,10 @@ def test_delete_session_removes_evidence_and_cache_but_refuses_an_open_recording
release()
response = asyncio.run(delete_route(session_id=session.name))
repeated_response = asyncio.run(delete_route(session_id=session.name))
assert response.status_code == 204
assert repeated_response.status_code == 204
assert not session.exists()
assert not recording.path.parent.exists()
with pytest.raises(SessionNotFoundError):
@@ -763,6 +766,90 @@ def test_cold_replay_returns_quick_202_then_status_returns_ready_launch(
manager.close()
def test_replay_post_retries_terminal_camera_finalization_failure(
tmp_path: Path,
) -> None:
repository = tmp_path / "repo"
sessions = repository / "sessions"
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
store = SessionStore(repository, data_dir=tmp_path / "data")
store.reconcile_archive(xgrids_k1_archive_source(sessions))
payload = b"recording-ready-before-camera-archive"
export_calls = 0
finalization_calls = 0
def export_recording(source: Path, destination: Path) -> dict[str, object]:
nonlocal export_calls
export_calls += 1
destination.write_bytes(payload)
return {
"source_sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
"rrd_sha256": hashlib.sha256(payload).hexdigest(),
"rrd_bytes": len(payload),
"timeline": "session_time",
"timeline_start_ns": 0,
"timeline_end_ns": 2_500_000_000,
}
def prepare_recorded_media(
_command: ReplayCommand,
_recording: MaterializedRecording,
) -> tuple[RecordedMediaManifest, ...]:
nonlocal finalization_calls
finalization_calls += 1
if finalization_calls == 1:
raise SessionIntegrityError("camera archive is still sealing")
return ()
materializer = SessionRecordingMaterializer(
store.data_dir,
exporter=export_recording,
)
manager = SessionRecordingPreparationManager(
materializer,
ready_preparer=prepare_recorded_media,
)
router = build_session_router(
store,
recording_materializer=materializer,
recording_preparation_manager=manager,
)
replay_route = endpoint(
router,
"/api/v1/observation-sessions/{session_id}/replay",
"POST",
)
try:
first = asyncio.run(replay_route(session_id=session.name, request=None))
assert first.status_code == 202
deadline = time.monotonic() + 2
failed = manager.status(session.name)
while failed is None or failed.state != "failed":
assert time.monotonic() < deadline
time.sleep(0.005)
failed = manager.status(session.name)
failed_preparation_id = failed.preparation_id
retry = asyncio.run(replay_route(session_id=session.name, request=None))
assert retry.status_code == 202
retry_document = json.loads(retry.body)
assert retry_document["preparation"]["preparation_id"] != failed_preparation_id
deadline = time.monotonic() + 2
ready = manager.status(session.name)
while ready is None or ready.state != "ready":
assert time.monotonic() < deadline
time.sleep(0.005)
ready = manager.status(session.name)
launch = asyncio.run(replay_route(session_id=session.name, request=None))
assert launch["launch"]["session_id"] == session.name
assert export_calls == 1
assert finalization_calls == 2
finally:
manager.close()
def test_catalog_read_never_prepares_a_cold_historical_session(tmp_path: Path) -> None:
repository = tmp_path / "repo"
sessions = repository / "sessions"