feat(perception): export bounded runtime observations outside heartbeat lane
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""Lossy, bounded observability export; never a lease/readiness authority.
|
||||
|
||||
The controller mounts one private directory for the existing host agent. There
|
||||
is one atomic current snapshot, no per-frame journal, credential or model data.
|
||||
Slow/broken filesystem I/O affects only this daemon, never the heartbeat lane.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from .streaming_lifecycle import StreamingLifecycle
|
||||
|
||||
SCHEMA = "missioncore.perception-runtime-observation/v1"
|
||||
MAX_BYTES = 8192
|
||||
|
||||
|
||||
def observation(runtime: StreamingLifecycle, profile_name: str) -> dict[str, object]:
|
||||
snapshot = runtime.snapshot()
|
||||
continuity = snapshot["input_continuity"]
|
||||
phase = continuity.get("phase", "active")
|
||||
state = snapshot["state"]
|
||||
if state == "running" and phase in ("waiting", "synchronizing"):
|
||||
state = phase
|
||||
with runtime.mailbox.condition:
|
||||
pending = len(runtime.mailbox.pending)
|
||||
allocated = runtime.mailbox.bytes
|
||||
return {
|
||||
"schema_version": SCHEMA,
|
||||
"observed_at_utc": datetime.now(UTC).isoformat(),
|
||||
"service_state": state,
|
||||
"profile_name": profile_name,
|
||||
"active_request_id": runtime.start.run_id,
|
||||
"input_state": phase,
|
||||
"wait_reason": continuity.get("reason") or "",
|
||||
"live_children": snapshot["live_children"],
|
||||
"input_pauses": continuity.get("pauses", 0),
|
||||
"pending_bundles": pending,
|
||||
# Includes decoder scratch + admitted active inputs, not only the queue.
|
||||
"buffer_bytes": allocated,
|
||||
"actuation_allowed": False,
|
||||
"realtime_qualified": False,
|
||||
}
|
||||
|
||||
|
||||
class RuntimeTelemetryPublisher:
|
||||
def __init__(self, runtime: StreamingLifecycle, path: Path, profile_name: str) -> None:
|
||||
if not 1 <= len(profile_name) <= 160 or not path.parent.is_dir():
|
||||
raise ValueError("telemetry needs a bounded profile name and existing directory")
|
||||
self.runtime, self.path, self.profile_name = runtime, path, profile_name
|
||||
self.failures = 0
|
||||
self.stop_event = threading.Event()
|
||||
self.thread = threading.Thread(target=self._run, name="runtime-observability", daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def publish(self) -> bool:
|
||||
temporary = self.path.with_name(self.path.name + ".tmp")
|
||||
try:
|
||||
raw = json.dumps(observation(self.runtime, self.profile_name), allow_nan=False).encode()
|
||||
if len(raw) > MAX_BYTES:
|
||||
raise ValueError("runtime observation exceeds bound")
|
||||
with temporary.open("wb") as stream:
|
||||
stream.write(raw)
|
||||
os.replace(temporary, self.path)
|
||||
return True
|
||||
except (OSError, ValueError):
|
||||
self.failures += 1
|
||||
return False
|
||||
|
||||
def _run(self) -> None:
|
||||
while not self.stop_event.is_set():
|
||||
self.publish()
|
||||
if self.stop_event.wait(1.0):
|
||||
break
|
||||
self.publish() # Terminal state is exported by this thread, not close().
|
||||
|
||||
def close(self) -> bool:
|
||||
self.stop_event.set()
|
||||
self.thread.join(timeout=0.2)
|
||||
return not self.thread.is_alive()
|
||||
Reference in New Issue
Block a user