fix(k1): stabilize live recovery and media admission
This commit is contained in:
+59
-11
@@ -355,24 +355,66 @@ def newly_finalized_recording_ids(
|
||||
return tuple(sorted(set(current_finalized) - known_finalized))
|
||||
|
||||
|
||||
def observation_archive_revision(roots: Iterable[Path]) -> tuple[tuple[object, ...], ...]:
|
||||
"""Return a cheap change fence for direct-child observation archives.
|
||||
|
||||
The K1 writer creates and retires ``.current_session`` in the archive root,
|
||||
and every session directory is a direct child of that same root. Those
|
||||
operations advance the directory metadata, while growing capture/media
|
||||
files do not. This lets the reconciler notice session start, completion and
|
||||
removal without recursively reopening tens of thousands of immutable
|
||||
evidence files every two seconds.
|
||||
"""
|
||||
|
||||
revisions: list[tuple[object, ...]] = []
|
||||
for configured_root in roots:
|
||||
root = configured_root.expanduser().resolve(strict=False)
|
||||
try:
|
||||
metadata = root.lstat()
|
||||
except OSError:
|
||||
revisions.append((str(root), None))
|
||||
continue
|
||||
revisions.append(
|
||||
(
|
||||
str(root),
|
||||
metadata.st_dev,
|
||||
metadata.st_ino,
|
||||
metadata.st_mtime_ns,
|
||||
metadata.st_ctime_ns,
|
||||
)
|
||||
)
|
||||
revisions.sort(key=lambda item: str(item[0]))
|
||||
return tuple(revisions)
|
||||
|
||||
|
||||
async def _recording_preparation_reconciler() -> None:
|
||||
"""Prepare sessions finalized during this process, never historical rows."""
|
||||
|
||||
known_finalized: set[str] | None = None
|
||||
reconciled_archive_revision: tuple[tuple[object, ...], ...] | None = None
|
||||
while True:
|
||||
try:
|
||||
await asyncio.to_thread(refresh_observation_catalog)
|
||||
finalized = set(await asyncio.to_thread(finalized_replayable_recording_ids))
|
||||
newly_finalized = newly_finalized_recording_ids(
|
||||
known_finalized,
|
||||
finalized,
|
||||
archive_revision = await asyncio.to_thread(
|
||||
observation_archive_revision,
|
||||
(archive.root for archive in plugin_environment.observation_archives),
|
||||
)
|
||||
if newly_finalized:
|
||||
await asyncio.to_thread(
|
||||
enqueue_replayable_recordings,
|
||||
newly_finalized,
|
||||
if archive_revision != reconciled_archive_revision:
|
||||
await asyncio.to_thread(refresh_observation_catalog)
|
||||
finalized = set(await asyncio.to_thread(finalized_replayable_recording_ids))
|
||||
newly_finalized = newly_finalized_recording_ids(
|
||||
known_finalized,
|
||||
finalized,
|
||||
)
|
||||
known_finalized = finalized
|
||||
if newly_finalized:
|
||||
await asyncio.to_thread(
|
||||
enqueue_replayable_recordings,
|
||||
newly_finalized,
|
||||
)
|
||||
known_finalized = finalized
|
||||
# Keep the pre-scan revision. If a writer changed the archive
|
||||
# during the expensive discovery, the next two-second check
|
||||
# observes the newer revision and performs one follow-up pass.
|
||||
reconciled_archive_revision = archive_revision
|
||||
recording_reconciler_readiness.record_success()
|
||||
except Exception as exc:
|
||||
# A transient filesystem/catalog failure must not permanently
|
||||
@@ -496,6 +538,12 @@ _CLOSED_WEBSOCKET_SEND_ERRORS = frozenset(
|
||||
}
|
||||
)
|
||||
|
||||
# This stream carries the comparatively heavy control-state snapshot, not
|
||||
# camera or point-cloud media. Action requests return their own result, and the
|
||||
# frontend also keeps a four-second REST fallback, so a two-second passive
|
||||
# cadence preserves status recovery without starving the live media sockets.
|
||||
DEVICE_PLUGIN_EVENT_POLL_INTERVAL_SECONDS = 2.0
|
||||
|
||||
|
||||
def _is_closed_websocket_send_error(exc: RuntimeError) -> bool:
|
||||
message = " ".join(str(exc).split())
|
||||
@@ -528,7 +576,7 @@ async def device_plugin_events(websocket: WebSocket, plugin_id: str) -> None:
|
||||
if _is_closed_websocket_send_error(exc):
|
||||
return
|
||||
raise
|
||||
await asyncio.sleep(0.5)
|
||||
await asyncio.sleep(DEVICE_PLUGIN_EVENT_POLL_INTERVAL_SECONDS)
|
||||
except (PluginNotFoundError, PluginActionNotFoundError):
|
||||
await websocket.close(code=1008, reason="Device plugin is not available")
|
||||
except (PluginExecutionError, PluginRuntimeUnavailableError):
|
||||
|
||||
Reference in New Issue
Block a user