feat(perception): gate cross-host graph with acknowledged source clocks

This commit is contained in:
DCCONSTRUCTIONS
2026-09-02 20:37:21 +03:00
parent d80df61a7e
commit 35b6cd9e9e
17 changed files with 1026 additions and 62 deletions
+56 -2
View File
@@ -69,6 +69,60 @@ class ClockProbe:
)
@dataclass(frozen=True)
class ClockReceipt:
"""The responder's view AFTER an authenticated echo of its issued probe.
Responder receive=t2 and send=t3 bracket only processing, NOT network RTT.
t1-t2 is a lower bound and t4-t3 an upper bound for initiator-minus-responder.
Never construct an ordinary reversed ClockProbe: that would invert causality.
"""
probe: ClockProbe
acknowledged_ns: int
def __post_init__(self) -> None:
_stamp(self.acknowledged_ns)
if (
not self.probe.remote_send_ns
<= self.acknowledged_ns
<= (self.probe.remote_receive_ns + 500_000_000)
):
raise ClockMappingError("clock acknowledgement outside bounded round trip")
@property
def local_clock_id(self) -> str:
return self.probe.remote_clock_id
@property
def remote_clock_id(self) -> str:
return self.probe.local_clock_id
@property
def nonce(self) -> str:
return self.probe.nonce
@property
def local_send_ns(self) -> int:
return self.probe.remote_receive_ns
@property
def local_receive_ns(self) -> int:
return self.acknowledged_ns
def bounds_at(
self, local_ns: int, *, rate_ppm: int, timestamp_error_ns: int
) -> tuple[int, int]:
_stamp(local_ns)
if local_ns < self.acknowledged_ns:
raise ClockMappingError("acknowledgement is from the future")
widen = _drift(local_ns - self.probe.remote_receive_ns, rate_ppm) + 2 * timestamp_error_ns
return (
self.probe.local_send_ns - self.probe.remote_receive_ns - widen,
self.probe.local_receive_ns - self.probe.remote_send_ns + widen,
)
@dataclass(frozen=True)
class ClockBounds:
local_clock_id: str
@@ -161,11 +215,11 @@ class ClockWindow:
timestamp_error_ns,
maximum_age_ns,
)
self.samples: deque[ClockProbe] = deque(maxlen=16)
self.samples: deque[ClockProbe | ClockReceipt] = deque(maxlen=16)
self.failed = False
self.last_receive_ns = -1
def add(self, sample: ClockProbe) -> ClockBounds:
def add(self, sample: ClockProbe | ClockReceipt) -> ClockBounds:
if self.failed:
raise ClockMappingError("clock session quarantined; explicit new session required")
if (
@@ -62,7 +62,13 @@ class InputContinuity:
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"):
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
+113 -1
View File
@@ -23,10 +23,12 @@ from . import streaming_wire as wire
from .realtime_contract import StreamStart
from .streaming_clock import ClockProbe
from .streaming_grpc import OPTIONS, StreamAccess
from .streaming_source_clock import SourceAnchor
SERVICE = "missioncore.perception.v1.StreamControl"
METHOD = f"/{SERVICE}/Poll"
MAX_CONTROL = 8192
REPORT_METHOD = f"/{SERVICE}/ReportClock"
def _document(raw: bytes) -> dict[str, Any]:
@@ -77,6 +79,7 @@ class StreamControlEndpoint:
*,
clock_id: str,
clock_ns: Callable[[], int] = time.monotonic_ns,
observe: Callable[[ClockProbe, int, SourceAnchor, bool], None] | None = None,
) -> None:
wire.identifier(clock_id)
self.activation, self.pending, self.clock_id, self.clock_ns = (
@@ -89,10 +92,16 @@ class StreamControlEndpoint:
self._lock = threading.Lock()
self._last_poll_ns = -1
self.accepted = self.rejected = 0
self.observe = observe
self._challenge: dict[str, Any] | None = None
def handler(self) -> Any:
return grpc.method_handlers_generic_handler(
SERVICE, {"Poll": grpc.unary_unary_rpc_method_handler(self.poll)}
SERVICE,
{
"Poll": grpc.unary_unary_rpc_method_handler(self.poll),
"ReportClock": grpc.unary_unary_rpc_method_handler(self.report_clock),
},
)
def _authenticate(self, context: Any) -> None:
@@ -163,6 +172,19 @@ class StreamControlEndpoint:
encoded = wire.canonical(response)
if len(encoded) > MAX_CONTROL:
raise ValueError("trusted control snapshot exceeds bound")
if self.observe is not None:
with self._lock:
self._challenge = {
key: response[key]
for key in (
"nonce",
"local_clock_id",
"local_send_ns",
"remote_clock_id",
"remote_receive_ns",
"remote_send_ns",
)
}
except (ValueError, RuntimeError):
await context.abort(
grpc.StatusCode.UNAVAILABLE, "local controller has no current offer"
@@ -171,6 +193,52 @@ class StreamControlEndpoint:
self.accepted += 1
return encoded
async def report_clock(self, raw: bytes, context: Any) -> bytes:
received = self.clock_ns()
try:
self._authenticate(context)
value = _document(raw)
if set(value) != {"probe", "anchor", "ended"} or self.observe is None:
raise ValueError("clock receipt not admitted")
evidence = value["probe"]
with self._lock:
challenge = self._challenge
if (
challenge is None
or not isinstance(evidence, dict)
or set(evidence) != {*challenge, "local_receive_ns"}
or any(evidence[k] != v for k, v in challenge.items())
):
raise ValueError("clock receipt is not the current issued challenge")
self._challenge = None # Single-use, including malformed/late receipts.
probe = ClockProbe(
evidence["local_clock_id"],
evidence["remote_clock_id"],
evidence["nonce"],
*(
wire.uint64(evidence[k])
for k in (
"local_send_ns",
"remote_receive_ns",
"remote_send_ns",
"local_receive_ns",
)
),
)
anchor = SourceAnchor.from_dict(value["anchor"])
self.observe(probe, received, anchor, value["ended"])
except (ValueError, TypeError):
self.rejected += 1
await context.abort(grpc.StatusCode.INVALID_ARGUMENT, "clock receipt rejected")
return b""
return wire.canonical(
{
"schema_version": "missioncore.stream-clock-receipt/v1",
"activation_binding": wire.binding(self.activation),
"nonce": probe.nonce,
}
)
class StreamControlClient:
def __init__(
@@ -196,6 +264,7 @@ class StreamControlClient:
),
)
self.call = self.channel.unary_unary(METHOD)
self.report_call = self.channel.unary_unary(REPORT_METHOD)
self.polling = False
async def poll(self) -> tuple[ClockProbe, StreamAccess | None]:
@@ -267,3 +336,46 @@ class StreamControlClient:
async def close(self) -> None:
await self.channel.close()
async def acknowledge(
self, probe: ClockProbe, anchor: SourceAnchor, *, ended: bool = False
) -> None:
if self.polling or probe.local_clock_id != self.clock_id or type(ended) is not bool:
raise ValueError("one scoped control request at a time")
self.polling = True
try:
evidence = {
name: getattr(probe, name)
for name in (
"nonce",
"local_clock_id",
"remote_clock_id",
)
}
evidence.update(
{
name: str(getattr(probe, name))
for name in (
"local_send_ns",
"remote_receive_ns",
"remote_send_ns",
"local_receive_ns",
)
}
)
response = _document(
await self.report_call(
wire.canonical({"probe": evidence, "anchor": anchor.to_dict(), "ended": ended}),
metadata=self.ticket.metadata(),
timeout=0.5,
wait_for_ready=False,
)
)
if response != {
"schema_version": "missioncore.stream-clock-receipt/v1",
"activation_binding": wire.binding(self.ticket.activation),
"nonce": probe.nonce,
}:
raise ValueError("clock receipt response binding mismatch")
finally:
self.polling = False
+17 -16
View File
@@ -291,22 +291,23 @@ class StreamingIngress:
except Exception as exc:
self.error = str(exc)
self.terminal = "failed"
if self.opened:
if self.runtime.continuity is not None and isinstance(
exc, (InputInterrupted, StreamSuspended)
):
self.terminal = "paused"
# A late old socket cannot pause a replacement connection.
with suppress(RuntimeError): # Already replaced, stopping, or fenced.
self.runtime.pause_input(
self.input_epoch,
"input-disconnected"
if isinstance(exc, InputInterrupted)
else "source-gap",
)
else:
self.runtime.mailbox.finish(self.error)
self.runtime.request_stop("failed")
if self.runtime.continuity is not None and isinstance(
exc, (InputInterrupted, StreamSuspended)
):
# Admission already consumed the single-use grant. A transport
# loss before the application OPEN is still WAIT, not an active
# owner stranded forever. A late old socket remains fenced.
self.terminal = "paused"
with suppress(RuntimeError): # Already replaced, stopping, or fenced.
self.runtime.pause_input(
self.input_epoch,
"input-disconnected"
if isinstance(exc, InputInterrupted)
else "source-gap",
)
elif self.opened:
self.runtime.mailbox.finish(self.error)
self.runtime.request_stop("failed")
finally:
self.connection.close()
if reservation is not None:
+12 -5
View File
@@ -223,11 +223,17 @@ class StreamingLifecycle:
return
self._check(self.start)
self.continuity.check(epoch, synchronizing=synchronizing)
if event_ns is not None:
assert self._source_clock_ns is not None
assert self._source_clock_ns is not None
try:
now = self._source_clock_ns()
if event_ns < self.continuity.cutoff_ns or not 0 <= now - event_ns <= 250_000_000:
raise StreamSuspended("obsolete or future input, no backlog replay")
except StreamSuspended:
self.continuity.pause("source-clock")
self.mailbox.pause()
raise
if event_ns is not None and (
event_ns < self.continuity.cutoff_ns or not 0 <= now - event_ns <= 250_000_000
):
raise StreamSuspended("obsolete or future input, no backlog replay")
def pause_input(self, epoch: StreamStart, reason: str) -> None:
with self._lock:
@@ -253,11 +259,12 @@ class StreamingLifecycle:
raise StreamSuspended("old epoch callbacks or connection still active")
if self.continuity.phase != "waiting":
raise StreamSuspended("pause required before a new input epoch")
source_now = self._source_clock_ns() # Check BEFORE mutating the paused mailbox.
self.mailbox.begin_epoch()
if self._ingress_thread is not None:
self._threads = [t for t in self._threads if t is not self._ingress_thread]
self._ingress_thread = None
return self.continuity.begin(self._source_clock_ns())
return self.continuity.begin(source_now)
def resume_input(
self, epoch: StreamStart, evidence: ResumeEvidence, reset_temporal: Callable[[], None]
@@ -0,0 +1,94 @@
"""Responder-owned source timeline and conditional clock readiness, no GPU authority."""
from __future__ import annotations
import threading
from dataclasses import dataclass
from . import streaming_wire as wire
from .streaming_clock import ClockMappingError, ClockProbe, ClockReceipt, ClockWindow, _stamp
from .streaming_continuity import StreamSuspended
@dataclass(frozen=True)
class SourceAnchor:
source_zero_ns: int
local_zero_ns: int
def __post_init__(self) -> None:
_stamp(self.source_zero_ns)
_stamp(self.local_zero_ns)
def to_dict(self) -> dict[str, str]:
return {
"source_zero_ns": str(self.source_zero_ns),
"local_zero_ns": str(self.local_zero_ns),
}
@classmethod
def from_dict(cls, value: object) -> SourceAnchor:
if not isinstance(value, dict) or set(value) != {"source_zero_ns", "local_zero_ns"}:
raise ValueError("invalid immutable source anchor")
return cls(wire.uint64(value["source_zero_ns"]), wire.uint64(value["local_zero_ns"]))
class SourceClockMonitor:
def __init__(self, worker_clock_id: str, source_zero_ns: int) -> None:
wire.identifier(worker_clock_id)
_stamp(source_zero_ns)
self.worker_clock_id, self.source_zero = worker_clock_id, source_zero_ns
self.wall_zero = 0 # Not a comparable host clock; use due()/observed().
self.anchor: SourceAnchor | None = None
self.window: ClockWindow | None = None
self.ended = False
self._lock = threading.RLock()
def observe(
self, probe: ClockProbe, received_ns: int, anchor: SourceAnchor, ended: bool
) -> None:
"""Called only after the transport verifies the one-use issued challenge."""
receipt = ClockReceipt(probe, received_ns)
if probe.remote_clock_id != self.worker_clock_id or type(ended) is not bool:
raise ValueError("foreign clock/status")
with self._lock:
if self.ended or anchor.source_zero_ns != self.source_zero:
raise ValueError("source activation ended or source anchor changed")
if self.anchor is None:
if not 0 <= anchor.local_zero_ns - probe.local_receive_ns <= 10_000_000_000:
raise ValueError("initial source start must be bounded and not in the past")
self.anchor = anchor
self.window = ClockWindow(
self.worker_clock_id,
probe.local_clock_id,
rate_ppm=500,
timestamp_error_ns=50_000,
)
elif anchor != self.anchor:
raise ValueError("source timeline is immutable for the activation")
assert self.window is not None
self.window.add(receipt)
self.ended = ended
def observed(self, now_ns: int) -> tuple[int, int]:
"""Source time midpoint + integer half-width, freshly checked at use."""
with self._lock:
try:
if self.window is None or self.anchor is None:
raise ClockMappingError("source clock not observed")
bounds = self.window.current(now_ns)
bounds.require(now_ns)
lower, upper = bounds.offset_at(now_ns)
stamp = self.source_zero + now_ns + (lower + upper) // 2 - self.anchor.local_zero_ns
_stamp(stamp)
return stamp, (upper - lower + 1) // 2
except ClockMappingError as exc:
raise StreamSuspended("source clock unavailable") from exc
def source_now(self, now_ns: int) -> int:
stamp, uncertainty = self.observed(now_ns)
# Admission/cutoff/resume use the upper age bound, never a younger estimate.
return stamp + uncertainty
def due(self, stamp: int, now_ns: int) -> tuple[int, int]:
observed, uncertainty = self.observed(now_ns)
return now_ns + stamp - observed, uncertainty