feat(archive): complete recorded session lifecycle
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user