99 lines
3.9 KiB
Python
99 lines
3.9 KiB
Python
"""Recoverable input epochs; resident model ownership is a separate lifetime.
|
|
|
|
Used only by the explicitly enabled resumable adapter. The controller supplies
|
|
the source-clock mapping and the profile adapter supplies verified decoder and
|
|
sensor evidence. No socket, GPU, vehicle command or unbounded history lives here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, replace
|
|
from hashlib import sha256
|
|
|
|
from .realtime_contract import StreamStart, _integer
|
|
|
|
|
|
class StreamSuspended(RuntimeError):
|
|
"""Discard obsolete work, keep the resident model processes and lease."""
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ResumeEvidence:
|
|
camera_ns: int
|
|
pose_ns: int
|
|
oldest_points_ns: int
|
|
newest_points_ns: int
|
|
decoded_keyframe: bool
|
|
|
|
def validate(self, *, cutoff_ns: int, source_now_ns: int) -> None:
|
|
for name in ("camera_ns", "pose_ns", "oldest_points_ns", "newest_points_ns"):
|
|
_integer(getattr(self, name), name)
|
|
_integer(source_now_ns, "source clock")
|
|
if (
|
|
self.decoded_keyframe is not True
|
|
or not cutoff_ns <= self.pose_ns <= self.camera_ns <= source_now_ns
|
|
or not cutoff_ns <= self.oldest_points_ns <= self.newest_points_ns <= self.camera_ns
|
|
or source_now_ns - self.camera_ns > 250_000_000
|
|
or self.camera_ns - self.pose_ns > 100_000_000
|
|
or self.camera_ns - self.newest_points_ns > 100_000_000
|
|
or self.camera_ns - self.oldest_points_ns > 250_000_000
|
|
or max(
|
|
abs(self.pose_ns - self.oldest_points_ns), abs(self.pose_ns - self.newest_points_ns)
|
|
)
|
|
> 100_000_000
|
|
):
|
|
raise StreamSuspended("fresh keyframe and causal camera/pose/points required")
|
|
|
|
|
|
class InputContinuity:
|
|
"""Access is serialized by StreamingLifecycle's lock."""
|
|
|
|
def __init__(self, activation: StreamStart) -> None:
|
|
self.activation = self.epoch = activation
|
|
self.phase = "active"
|
|
self.generation = self.pauses = 0
|
|
self.reason: str | None = None
|
|
self.cutoff_ns = 0
|
|
|
|
def check(self, epoch: StreamStart, *, synchronizing: bool = False) -> None:
|
|
if epoch != self.epoch:
|
|
raise StreamSuspended("obsolete input epoch")
|
|
if self.phase != "active" and not (synchronizing and self.phase == "synchronizing"):
|
|
raise StreamSuspended("input is waiting for resynchronization")
|
|
|
|
def pause(self, reason: str) -> None:
|
|
if reason not in (
|
|
"input-disconnected",
|
|
"input-timeout",
|
|
"source-gap",
|
|
"worker-telemetry",
|
|
"source-clock",
|
|
):
|
|
raise ValueError("unknown recoverable pause reason")
|
|
if self.phase != "waiting":
|
|
self.pauses += 1
|
|
self.phase, self.reason = "waiting", reason
|
|
|
|
def begin(self, source_now_ns: int) -> StreamStart:
|
|
_integer(source_now_ns, "source clock")
|
|
if self.phase != "waiting" or source_now_ns < self.cutoff_ns:
|
|
raise StreamSuspended("new input needs a pause and nonregressing source clock")
|
|
self.generation += 1
|
|
identity = sha256(f"{self.activation.epoch_id}:{self.generation}".encode()).hexdigest()
|
|
# Wire v2 already binds every packet to the complete StreamStart. Change
|
|
# the input epoch, NOT the resident lease generation/model activation.
|
|
self.epoch = replace(self.activation, epoch_id="input-" + identity)
|
|
self.cutoff_ns, self.phase = source_now_ns, "synchronizing"
|
|
return self.epoch
|
|
|
|
def snapshot(self) -> dict[str, object]:
|
|
return {
|
|
"phase": self.phase,
|
|
"reason": self.reason,
|
|
"input_generation": self.generation,
|
|
"pauses": self.pauses,
|
|
"input_start": self.epoch.to_dict(),
|
|
"minimum_source_ns": str(self.cutoff_ns),
|
|
"actuation_allowed": False,
|
|
}
|