fix(archive): preserve prepared sessions across restarts
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user