fix(archive): preserve prepared sessions across restarts

This commit is contained in:
DCCONSTRUCTIONS
2026-07-18 11:08:27 +03:00
parent aa3680948f
commit 574a494759
13 changed files with 398 additions and 132 deletions
+76 -7
View File
@@ -111,6 +111,11 @@ class SessionRecordingPreparationManager:
tuple[RecordedMediaManifest, ...],
]
| None = None,
ready_restorer: Callable[
[ReplayCommand, MaterializedRecording],
tuple[RecordedMediaManifest, ...] | None,
]
| None = None,
) -> None:
if queue_capacity < 1:
raise ValueError("recording preparation queue capacity must be positive")
@@ -123,6 +128,7 @@ class SessionRecordingPreparationManager:
# hung exporter must become observable to the browser stall detector.
self._heartbeat_interval_seconds = heartbeat_interval_seconds
self._ready_preparer = ready_preparer
self._ready_restorer = ready_restorer
self._guard = threading.RLock()
self._current_by_session: dict[str, _PreparationJob] = {}
self._closed = True
@@ -237,6 +243,74 @@ class SessionRecordingPreparationManager:
self._current_by_session[command.session_id] = ready
return self._snapshot_locked(ready)
def restore_published(
self,
command: ReplayCommand,
) -> RecordingPreparationSnapshot | None:
"""Restore a complete durable package without enqueueing preparation.
The RRD and every recorded-media descriptor must already have been
published by an earlier successful job. Missing/stale pieces return a
cold miss; this method never invokes an exporter or media parser.
"""
source_command = _source_command(command)
identity = _source_identity(source_command)
with self._guard:
current = self._current_by_session.get(command.session_id)
if (
current is not None
and current.source_identity == identity
and current.state in ACTIVE_PREPARATION_STATES | {"ready"}
):
return self._snapshot_locked(current)
recording = self.materializer.restore_published(source_command)
if recording is None:
with self._guard:
current = self._current_by_session.get(command.session_id)
if current is not None and current.state == "ready":
self._current_by_session.pop(command.session_id, None)
return None
if self._ready_preparer is None:
recorded_media: tuple[RecordedMediaManifest, ...] = ()
else:
if self._ready_restorer is None:
return None
restored_media = self._ready_restorer(source_command, recording)
if restored_media is None:
return None
recorded_media = restored_media
validate_recorded_media_timeline(
recorded_media,
recording_start_seconds=recording.timeline_start_ns / 1_000_000_000,
recording_end_seconds=recording.timeline_end_ns / 1_000_000_000,
)
with self._guard:
current = self._current_by_session.get(command.session_id)
if (
current is not None
and current.source_identity == identity
and current.state in ACTIVE_PREPARATION_STATES | {"ready"}
):
if current.state == "ready":
current.recording = recording
current.recorded_media = recorded_media
return self._snapshot_locked(current)
ready = _PreparationJob(
preparation_id=uuid4().hex,
source_identity=identity,
command=source_command,
state="ready",
progress=1.0,
recording=recording,
recorded_media=recorded_media,
)
self._current_by_session[command.session_id] = ready
return self._snapshot_locked(ready)
def resolve_cached_pinned(
self,
command: ReplayCommand,
@@ -325,10 +399,7 @@ class SessionRecordingPreparationManager:
job is None
or job.state != "ready"
or job.recording is None
or (
preparation_id is not None
and job.preparation_id != preparation_id
)
or (preparation_id is not None and job.preparation_id != preparation_id)
):
return None
recording = job.recording
@@ -456,9 +527,7 @@ class SessionRecordingPreparationManager:
recording_start_seconds=(
recording.timeline_start_ns / 1_000_000_000
),
recording_end_seconds=(
recording.timeline_end_ns / 1_000_000_000
),
recording_end_seconds=(recording.timeline_end_ns / 1_000_000_000),
)
except Exception:
with self._guard: