fix(replay): recover failed archives and release deleted sessions
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user