feat(archive): complete recorded session lifecycle

This commit is contained in:
DCCONSTRUCTIONS
2026-07-19 01:07:21 +03:00
parent ffffee1879
commit 71c85e9894
22 changed files with 922 additions and 144 deletions
+95 -9
View File
@@ -87,6 +87,13 @@ class _WorkerGeneration:
worker: threading.Thread | None = None
@dataclass(slots=True)
class _LaunchReservation:
preparation_id: str
release: Callable[[], None]
timer: threading.Timer
class SessionRecordingPreparationManager:
"""One bounded, process-owned conversion worker for durable recordings.
@@ -131,6 +138,7 @@ class SessionRecordingPreparationManager:
self._ready_restorer = ready_restorer
self._guard = threading.RLock()
self._current_by_session: dict[str, _PreparationJob] = {}
self._launch_reservations: dict[str, _LaunchReservation] = {}
self._closed = True
self._generation_counter = 0
self._active_generation: _WorkerGeneration | None = None
@@ -379,10 +387,7 @@ class SessionRecordingPreparationManager:
if pinned is None:
return None
snapshot, release = pinned
timer = threading.Timer(lease_seconds, release)
timer.name = f"missioncore-recording-launch-lease-{snapshot.preparation_id}"
timer.daemon = True
timer.start()
self._reserve_launch_lease(snapshot, release, lease_seconds)
return snapshot
def pin_ready(
@@ -393,6 +398,7 @@ class SessionRecordingPreparationManager:
) -> tuple[RecordingPreparationSnapshot, Callable[[], None]] | None:
"""Cheaply lease the exact already-validated ready generation."""
launch_release: Callable[[], None] | None = None
with self._guard:
job = self._current_by_session.get(session_id)
if (
@@ -409,7 +415,17 @@ class SessionRecordingPreparationManager:
self._current_by_session.pop(session_id, None)
return None
snapshot = self._snapshot_locked(job)
return snapshot, pinned
reservation = self._launch_reservations.get(session_id)
if (
reservation is not None
and reservation.preparation_id == snapshot.preparation_id
):
self._launch_reservations.pop(session_id, None)
reservation.timer.cancel()
launch_release = reservation.release
if launch_release is not None:
launch_release()
return snapshot, pinned
def reserve_ready(
self,
@@ -426,12 +442,26 @@ class SessionRecordingPreparationManager:
if pinned is None:
return None
snapshot, release = pinned
timer = threading.Timer(lease_seconds, release)
timer.name = f"missioncore-recording-launch-lease-{snapshot.preparation_id}"
timer.daemon = True
timer.start()
self._reserve_launch_lease(snapshot, release, lease_seconds)
return snapshot
def release_launch_reservation(self, session_id: str) -> bool:
"""Release an unused launch-to-GET lease for one session.
The first matching recording GET consumes this reservation after it
acquires its own response-lifetime pin. Deletion may release a launch
reservation that was never consumed; an active response pin remains
independently protected by the materializer.
"""
with self._guard:
reservation = self._launch_reservations.pop(session_id, None)
if reservation is None:
return False
reservation.timer.cancel()
reservation.release()
return True
def cancel(self, session_id: str, *, preparation_id: str | None = None) -> bool:
with self._guard:
job = self._current_by_session.get(session_id)
@@ -447,11 +477,30 @@ class SessionRecordingPreparationManager:
self._transition_locked(job, "cancelled", job.progress)
return True
def discard(self, session_id: str) -> bool:
"""Forget one non-active job before its source session is deleted."""
with self._guard:
job = self._current_by_session.get(session_id)
if job is not None and job.state in ACTIVE_PREPARATION_STATES:
return False
self._current_by_session.pop(session_id, None)
reservation = self._launch_reservations.pop(session_id, None)
if reservation is not None:
reservation.timer.cancel()
if reservation is not None:
reservation.release()
return True
def close(self, *, timeout: float = 5.0) -> None:
with self._guard:
if self._closed:
return
self._closed = True
reservations = tuple(self._launch_reservations.values())
self._launch_reservations.clear()
for reservation in reservations:
reservation.timer.cancel()
for job in self._current_by_session.values():
if job.state in ACTIVE_PREPARATION_STATES:
job.interrupted_by_restart = not job.cancelled_by_operator
@@ -466,9 +515,46 @@ class SessionRecordingPreparationManager:
pending.stop_event.set()
self._pending_generation = None
worker = None if active is None else active.worker
for reservation in reservations:
reservation.release()
if worker is not None:
worker.join(timeout=max(0.0, timeout))
def _reserve_launch_lease(
self,
snapshot: RecordingPreparationSnapshot,
release: Callable[[], None],
lease_seconds: float,
) -> None:
timer = threading.Timer(
lease_seconds,
self._expire_launch_reservation,
args=(snapshot.session_id, snapshot.preparation_id),
)
timer.name = f"missioncore-recording-launch-lease-{snapshot.preparation_id}"
timer.daemon = True
reservation = _LaunchReservation(
preparation_id=snapshot.preparation_id,
release=release,
timer=timer,
)
with self._guard:
previous = self._launch_reservations.pop(snapshot.session_id, None)
if previous is not None:
previous.timer.cancel()
self._launch_reservations[snapshot.session_id] = reservation
if previous is not None:
previous.release()
timer.start()
def _expire_launch_reservation(self, session_id: str, preparation_id: str) -> None:
with self._guard:
reservation = self._launch_reservations.get(session_id)
if reservation is None or reservation.preparation_id != preparation_id:
return
self._launch_reservations.pop(session_id, None)
reservation.release()
def _run_generation(self, generation: _WorkerGeneration) -> None:
work_queue = generation.work_queue
try: