feat(perception): add bounded binary stream ingress

This commit is contained in:
DCCONSTRUCTIONS
2026-09-02 12:21:25 +03:00
parent 85349f42f9
commit da60feff90
6 changed files with 1262 additions and 1 deletions
+293
View File
@@ -0,0 +1,293 @@
"""One controller-supplied IPC socket feeding the common streaming lifecycle.
No listener, acquisition, archive reader, model RPC, network authentication or
automatic reconnect. Reconnect needs a new StreamStart/lease and decoder state.
The synchronous consumer is a trusted adapter: raw events are borrowed for that
call only. It must reserve decoder scratch before allocation and retain decoded
work only through the common mailbox. Slow/failed consumers fail this stream;
they do not create another buffering queue.
"""
from __future__ import annotations
import select
import socket
import threading
import time
from collections import Counter
from collections.abc import Callable
from hashlib import sha256
from typing import Any
from k1link.compute.live_perception import LiveIngressEvent
from . import streaming_wire as wire
from .streaming_lifecycle import StreamingLifecycle
class StreamingIngress:
def __init__(
self,
connection: socket.socket,
runtime: StreamingLifecycle,
session_id: str,
session_generation: int,
consume: Callable[[LiveIngressEvent], None],
notice: Callable[[dict[str, Any]], None],
*,
fragment_timeout: float = 0.25,
idle_timeout: float = 2.0,
) -> None:
if not 0 < fragment_timeout <= 2 or not 0 < idle_timeout <= 30:
raise ValueError("invalid bounded ingress timeouts")
wire.identifier(session_id)
wire.decimal(session_generation)
if session_generation < 1:
raise ValueError("invalid acquisition generation")
self.connection, self.runtime = connection, runtime
connection.setblocking(False)
self.session_id, self.session_generation = session_id, session_generation
self.consume, self.notice = consume, notice
self.fragment_timeout, self.idle_timeout = fragment_timeout, idle_timeout
self.binding = wire.binding(runtime.start)
self.opened = False
self.error: str | None = None
self.terminal: str | None = None
self.counts: Counter[str] = Counter()
self.last_ingress_sequence = 0
self.channel_sequences: dict[str, tuple[str, int, int]] = {}
self.camera_initialized = False
self.thread = threading.Thread(
target=self._serve, name="perception-binary-ingress", daemon=True
)
runtime.track_thread(self.thread)
def start(self) -> None:
self.runtime.check_current(self.runtime.start)
self.thread.start()
def join(self, timeout: float = 1.0) -> bool:
self.thread.join(timeout=timeout)
return not self.thread.is_alive()
def _check(self, deadline: float) -> None:
self.runtime.check_current(self.runtime.start)
if time.monotonic() >= deadline:
raise wire.StreamWireError("ingress deadline exceeded")
def _read_into(self, target: memoryview, deadline: float) -> None:
offset = 0
while offset < len(target):
self._check(deadline)
if not select.select(
[self.connection], [], [], min(0.05, max(0, deadline - time.monotonic()))
)[0]:
continue
try:
read = self.connection.recv_into(target[offset:])
except BlockingIOError:
continue
if read == 0:
raise wire.StreamWireError("unexpected EOF without explicit End")
self.counts["wire_bytes"] += read
offset += read
self._check(deadline)
def _header(self, deadline: float) -> tuple[int, dict[str, Any]]:
prefix = bytearray(wire.PREFIX.size)
self._read_into(memoryview(prefix), deadline)
magic, kind, size = wire.PREFIX.unpack(prefix)
if (
magic != wire.MAGIC
or kind not in (wire.OPEN, wire.FRAGMENT, wire.END, wire.CANCEL, wire.GAP)
or not 0 < size <= wire.MAX_HEADER
):
raise wire.StreamWireError("invalid frame prefix or header size")
raw = bytearray(size)
self._read_into(memoryview(raw), deadline)
return kind, wire.parse_header(raw)
def _bound(self, header: dict[str, Any]) -> None:
if header.get("binding") != self.binding:
raise wire.StreamWireError("frame belongs to another StreamStart")
self.runtime.check_current(self.runtime.start)
def _fragment(self, header: dict[str, Any]) -> tuple[dict[str, Any], int, int, int]:
if set(header) != {
"binding",
"event",
"total_bytes",
"offset",
"fragment_bytes",
"payload_sha256",
"fragment_sha256",
}:
raise wire.StreamWireError("fragment fields changed")
self._bound(header)
event = wire.validate_event_metadata(header["event"])
if (
event["session_id"] != self.session_id
or event["session_generation"] != self.session_generation
):
raise wire.StreamWireError("event acquisition binding changed")
total, offset, size = (header[x] for x in ("total_bytes", "offset", "fragment_bytes"))
if any(type(x) is not int for x in (total, offset, size)) or not (
0 < total <= wire.PAYLOAD_LIMITS[event["modality"]]
and 0 <= offset < total
and 0 < size <= wire.MAX_FRAGMENT
and size == min(wire.MAX_FRAGMENT, total - offset)
):
raise wire.StreamWireError("invalid fragment length or offset")
wire.digest(header["payload_sha256"])
wire.digest(header["fragment_sha256"])
return event, total, offset, size
def _observation(self, first: dict[str, Any], deadline: float) -> None:
event, total, offset, size = self._fragment(first)
modality = event["modality"]
if offset != 0 or event["ingress_sequence"] <= self.last_ingress_sequence:
raise wire.StreamWireError("duplicate, out-of-order or incomplete observation")
previous = self.channel_sequences.get(modality)
if previous and (
event["source_id"] != previous[0]
or event["source_sequence"] <= previous[1]
or event["received_monotonic_ns"] < previous[2]
):
raise wire.StreamWireError("channel identity, sequence or source clock regressed")
if modality == "camera-frame" and not self.camera_initialized:
raise wire.StreamWireError("camera frame requires new codec initialization")
if modality == "camera-frame" and previous and event["source_sequence"] != previous[1] + 1:
raise wire.StreamWireError("camera sequence gap requires new codec initialization")
self.counts["observations_started"] += 1
reservation = self.runtime.mailbox.reserve_ingress(2 * total)
raw = None
admitted = None
try:
raw = bytearray(total)
header = first
while True:
chunk = memoryview(raw)[offset : offset + size]
try:
self._read_into(chunk, deadline)
if sha256(chunk).hexdigest() != header["fragment_sha256"]:
raise wire.StreamWireError("fragment integrity mismatch")
finally:
chunk.release()
self.counts["fragments"] += 1
offset += size
if offset == total:
break
kind, header = self._header(deadline)
if kind != wire.FRAGMENT:
raise wire.StreamWireError("incomplete observation before control event")
next_event, next_total, next_offset, size = self._fragment(header)
if (
next_event != event
or next_total != total
or next_offset != offset
or header["payload_sha256"] != first["payload_sha256"]
):
raise wire.StreamWireError("interleaved or discontinuous fragments")
if sha256(raw).hexdigest() != first["payload_sha256"]:
raise wire.StreamWireError("observation integrity mismatch")
self._check(deadline)
admitted = LiveIngressEvent(**event, payload=bytes(raw))
raw = None # Immutable callback bytes remain fully reserved.
self.consume(admitted)
self._check(deadline)
self.counts["ingress_sequence_gaps"] += (
event["ingress_sequence"] - self.last_ingress_sequence - 1
)
self.last_ingress_sequence = event["ingress_sequence"]
self.channel_sequences[modality] = (
event["source_id"],
event["source_sequence"],
event["received_monotonic_ns"],
)
if modality == "camera-init":
self.camera_initialized = True
self.channel_sequences.pop("camera-frame", None)
self.counts["observations_completed"] += 1
finally:
admitted = raw = None
reservation.release()
def _gap(self, header: dict[str, Any]) -> None:
if set(header) != {"binding", "modality", "reason", "count"}:
raise wire.StreamWireError("gap fields changed")
if header["modality"] not in wire.PAYLOAD_LIMITS or header["reason"] not in (
"unavailable",
"source-gap",
"overload",
):
raise wire.StreamWireError("invalid gap notice")
self.counts["declared_gap_observations"] += wire.uint64(header["count"])
if header["modality"].startswith("camera"):
self.camera_initialized = False
self.notice(dict(header))
self.runtime.check_current(self.runtime.start)
self.counts["gap_notices"] += 1
def _serve(self) -> None:
reservation = None
try:
# Covers raw header + JSON decoding copies; Python object overhead
# belongs to the separately measured RSS envelope, not payload bytes.
reservation = self.runtime.mailbox.reserve_ingress(
3 * wire.MAX_HEADER + wire.PREFIX.size
)
kind, header = self._header(time.monotonic() + self.idle_timeout)
expected = wire.parse_header(
bytearray(
wire.open_packet(
self.runtime.start,
self.session_id,
self.session_generation,
)[wire.PREFIX.size :]
)
)
if kind != wire.OPEN or header != expected:
raise wire.StreamWireError("StreamStart or acquisition handshake mismatch")
self.opened = True
while True:
kind, header = self._header(time.monotonic() + self.idle_timeout)
self._bound(header)
if kind == wire.FRAGMENT:
self._observation(header, time.monotonic() + self.fragment_timeout)
elif kind == wire.GAP:
self._gap(header)
elif kind in (wire.END, wire.CANCEL) and set(header) == {"binding"}:
self.terminal = "cancelled" if kind == wire.CANCEL else "end"
if kind == wire.CANCEL:
self.runtime.request_stop("cancelled")
else:
self.runtime.mailbox.finish()
return
else:
raise wire.StreamWireError("unexpected stream control event")
except Exception as exc:
self.error = str(exc)
self.terminal = "failed"
if self.opened:
self.runtime.mailbox.finish(self.error)
self.runtime.request_stop("failed")
finally:
self.connection.close()
if reservation is not None:
reservation.release()
def snapshot(self) -> dict[str, Any]:
return {
"schema_version": wire.SCHEMA,
"opened": self.opened,
"terminal": self.terminal,
"error": self.error,
"counts": dict(self.counts),
"incomplete_observations": self.counts["observations_started"]
- self.counts["observations_completed"],
"source_duration_known": False,
"requires_eof_before_delivery": False,
"transport": "controller-supplied-ipc-socket",
"network_qualified": False,
"actuation_allowed": False,
}
+42 -1
View File
@@ -17,6 +17,21 @@ StreamBundle = Mapping[str, Any]
DISCARD_REASONS = frozenset(("cancelled", "gpu-failed", "processing-failed"))
class IngressReservation:
"""Raw/reassembly bytes retained outside the decoded work queue.
Trusted adapters reserve BEFORE allocation and release AFTER borrowed raw
buffers/callbacks are gone. Closing ingress never frees a live reservation.
This is byte ownership, not another pending camera slot.
"""
def __init__(self, mailbox: StreamMailbox, size: int) -> None:
self._mailbox, self.size = mailbox, size
def release(self) -> None:
self._mailbox._release_ingress(self)
class StreamMailbox:
def __init__(self, capacity: int = 2, byte_limit: int = 16 * 1024 * 1024) -> None:
if type(capacity) is not int or not 1 <= capacity <= 2:
@@ -31,6 +46,7 @@ class StreamMailbox:
self._recent_drops: deque[dict[str, Any]] = deque(maxlen=256)
self._owned: dict[int, tuple[StreamBundle, int]] = {}
self._active: set[int] = set()
self._ingress: dict[IngressReservation, int] = {}
self._last_sequence = -1
self.done = False
self.error: str | None = None
@@ -49,7 +65,32 @@ class StreamMailbox:
@property
def quiescent(self) -> bool:
with self.condition:
return self.done and not self._owned and not self.external_pending
return self.done and not self._owned and not self.external_pending and not self._ingress
def reserve_ingress(self, size: int) -> IngressReservation:
"""Charge raw buffers to the SAME budget as queued and active inputs.
Fail closed on exhaustion; do not wait and silently stretch source time.
Decoder scratch must also be reserved before allocation; retained decoded
work uses the existing bundle ownership. No adapter may hide a buffer.
"""
if type(size) is not int or size <= 0:
raise ValueError("invalid ingress reservation")
with self.condition:
if self.done or self.bytes + size > self.byte_limit or len(self._ingress) >= 8:
raise ValueError("ingress byte budget exhausted or closed")
reservation = IngressReservation(self, size)
self._ingress[reservation] = size
self.bytes += size
self.peak_bytes = max(self.peak_bytes, self.bytes)
return reservation
def _release_ingress(self, reservation: IngressReservation) -> None:
with self.condition:
if reservation not in self._ingress:
raise ValueError("ingress release ownership mismatch")
self.bytes -= self._ingress.pop(reservation)
self.condition.notify_all()
def _drop(self, bundle: StreamBundle, reason: str) -> None:
self.drop_counts[reason] += 1
+91
View File
@@ -0,0 +1,91 @@
"""Synchronous, deadline-bounded IPC writer for existing raw ingress events.
The caller owns pacing and already-committed source bytes. No background queue,
recording inventory or retry is created; timeout is an explicit failed source
outcome. A new connection requires a new controller-approved StreamStart.
"""
from __future__ import annotations
import select
import socket
import time
from collections.abc import Callable, Iterable
from k1link.compute.live_perception import LiveIngressEvent
from . import streaming_wire as wire
from .realtime_contract import StreamStart
class StreamingSender:
def __init__(
self,
connection: socket.socket,
start: StreamStart,
session_id: str,
session_generation: int,
check_current: Callable[[], None],
*,
timeout: float = 0.25,
) -> None:
if not 0 < timeout <= 2:
raise ValueError("invalid bounded send timeout")
self.connection, self.identity, self.check_current = connection, start, check_current
self.session_id, self.session_generation = session_id, session_generation
self.timeout, self.closed = timeout, False
connection.setblocking(False)
try:
self._send((wire.open_packet(start, session_id, session_generation),))
except Exception:
self.close()
raise
def _send(self, pieces: Iterable[bytes | memoryview]) -> None:
if self.closed:
raise wire.StreamWireError("sender is closed")
deadline = time.monotonic() + self.timeout
try:
for piece in pieces:
view = memoryview(piece)
offset = 0
while offset < len(view):
self.check_current()
remaining = deadline - time.monotonic()
if remaining <= 0:
raise wire.StreamWireError("send deadline exceeded")
if not select.select([], [self.connection], [], min(0.05, remaining))[1]:
continue
try:
count = self.connection.send(view[offset:])
except BlockingIOError:
continue
if count == 0:
raise wire.StreamWireError("source connection closed")
offset += count
self.check_current()
if time.monotonic() >= deadline:
raise wire.StreamWireError("send deadline exceeded")
except Exception:
self.close()
raise
def send(self, event: LiveIngressEvent) -> None:
if (
event.session_id != self.session_id
or event.session_generation != self.session_generation
):
self.close()
raise wire.StreamWireError("source acquisition binding changed")
self._send(piece for pair in wire.event_packets(self.identity, event) for piece in pair)
def gap(self, *, modality: str, reason: str, count: int) -> None:
self._send((wire.gap_packet(self.identity, modality=modality, reason=reason, count=count),))
def end(self, *, cancel: bool = False) -> None:
self._send((wire.terminal_packet(self.identity, cancel=cancel),))
self.close()
def close(self) -> None:
self.closed = True
self.connection.close()
+211
View File
@@ -0,0 +1,211 @@
"""Bounded v2 mapping of existing raw-first LiveIngressEvent semantics.
This IPC candidate carries binary payloads, not base64/PNG/archive uploads.
It is not the selected/authenticated network transport. A trusted controller
binds one acquisition session to StreamStart; hashes provide integrity, NOT
authentication. No peer-supplied paths, commands, models or calibration loading.
"""
from __future__ import annotations
import json
import re
import struct
from collections.abc import Iterator
from hashlib import sha256
from typing import Any, Final
from k1link.compute.live_perception import LiveIngressEvent
from .realtime_contract import StreamStart
SCHEMA: Final = "missioncore.live-perception-wire/v2"
MAGIC: Final = b"MCI2"
PREFIX: Final = struct.Struct("!4sBI")
OPEN, FRAGMENT, END, CANCEL, GAP = range(1, 6)
MAX_HEADER: Final = 64 * 1024
MAX_FRAGMENT: Final = 1024 * 1024
PAYLOAD_LIMITS: Final = {
"camera-init": MAX_FRAGMENT,
"camera-frame": MAX_FRAGMENT,
"lidar": 2 * MAX_FRAGMENT,
"pose": 2 * MAX_FRAGMENT,
}
EVENT_FIELDS: Final = frozenset(
(
"ingress_sequence",
"session_id",
"session_generation",
"modality",
"source_id",
"source_sequence",
"captured_at_epoch_ns",
"received_monotonic_ns",
)
)
INTEGER_FIELDS: Final = EVENT_FIELDS - {"session_id", "modality", "source_id"}
_DECIMAL: Final = re.compile(r"^(0|[1-9][0-9]{0,19})$")
_DIGEST: Final = re.compile(r"^[a-f0-9]{64}$")
class StreamWireError(ValueError):
"""Malformed, stale, oversized or incomplete transport input."""
def uint64(value: object) -> int:
if not isinstance(value, str) or not _DECIMAL.fullmatch(value):
raise StreamWireError("wire uint64 must be a canonical decimal string")
result = int(value)
if result > (1 << 64) - 1:
raise StreamWireError("wire uint64 overflow")
return result
def decimal(value: object) -> str:
if type(value) is not int or not 0 <= value < 1 << 64:
raise StreamWireError("invalid uint64")
return str(value)
def identifier(value: object) -> str:
if not isinstance(value, str) or not 1 <= len(value) <= 160:
raise StreamWireError("invalid bounded source identity")
if not value.isascii() or any(ord(c) < 33 or ord(c) > 126 for c in value):
raise StreamWireError("invalid bounded source identity")
return value
def digest(value: object) -> str:
if not isinstance(value, str) or not _DIGEST.fullmatch(value):
raise StreamWireError("invalid payload digest")
return value
def canonical(value: dict[str, Any]) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
def start_document(start: StreamStart) -> dict[str, Any]:
result = start.to_dict()
result["lease_generation"] = decimal(start.lease_generation)
return result
def binding(start: StreamStart) -> str:
return sha256(canonical(start_document(start))).hexdigest()
def _unique_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in pairs:
if key in result:
raise StreamWireError("duplicate metadata field")
result[key] = value
return result
def parse_header(raw: bytearray) -> dict[str, Any]:
try:
value = json.loads(raw, object_pairs_hook=_unique_pairs)
except (ValueError, UnicodeError, RecursionError) as exc:
raise StreamWireError("invalid bounded metadata") from exc
if not isinstance(value, dict):
raise StreamWireError("metadata must be an object")
return value
def packet(kind: int, header: dict[str, Any]) -> bytes:
raw = canonical(header)
if kind not in (OPEN, FRAGMENT, END, CANCEL, GAP) or not 0 < len(raw) <= MAX_HEADER:
raise StreamWireError("invalid frame header")
return PREFIX.pack(MAGIC, kind, len(raw)) + raw
def open_packet(start: StreamStart, session_id: str, session_generation: int) -> bytes:
return packet(
OPEN,
{
"schema_version": SCHEMA,
"start": start_document(start),
"session_id": identifier(session_id),
"session_generation": decimal(session_generation),
},
)
def event_metadata(event: LiveIngressEvent) -> dict[str, Any]:
result = {name: getattr(event, name) for name in EVENT_FIELDS}
for name in INTEGER_FIELDS:
result[name] = decimal(result[name])
validate_event_metadata(result)
return result
def validate_event_metadata(value: object) -> dict[str, Any]:
if not isinstance(value, dict) or set(value) != EVENT_FIELDS:
raise StreamWireError("event fields changed")
result = dict(value)
for name in INTEGER_FIELDS:
result[name] = uint64(value[name])
for name in ("session_id", "source_id"):
identifier(value[name])
if (
not isinstance(value["modality"], str)
or value["modality"] not in PAYLOAD_LIMITS
or result["ingress_sequence"] < 1
or result["session_generation"] < 1
):
raise StreamWireError("invalid raw modality or generation")
return result
def event_packets(
start: StreamStart,
event: LiveIngressEvent,
) -> Iterator[tuple[bytes, memoryview]]:
"""Iterate one already committed raw event; never assemble a route/archive.
Source adapter owns the original event bytes. Returned views borrow them;
sender must finish each fragment before advancing this iterator.
"""
metadata = event_metadata(event)
total = len(event.payload)
if not 0 < total <= PAYLOAD_LIMITS[event.modality]:
raise StreamWireError("raw event exceeds modality bound")
whole_hash = sha256(event.payload).hexdigest()
view = memoryview(event.payload)
for offset in range(0, total, MAX_FRAGMENT):
chunk = view[offset : offset + MAX_FRAGMENT]
yield (
packet(
FRAGMENT,
{
"binding": binding(start),
"event": metadata,
"total_bytes": total,
"offset": offset,
"fragment_bytes": len(chunk),
"payload_sha256": whole_hash,
"fragment_sha256": sha256(chunk).hexdigest(),
},
),
chunk,
)
def terminal_packet(start: StreamStart, *, cancel: bool = False) -> bytes:
return packet(CANCEL if cancel else END, {"binding": binding(start)})
def gap_packet(start: StreamStart, *, modality: str, reason: str, count: int) -> bytes:
if modality not in PAYLOAD_LIMITS or reason not in ("unavailable", "source-gap", "overload"):
raise StreamWireError("invalid gap notice")
return packet(
GAP,
{
"binding": binding(start),
"modality": modality,
"reason": reason,
"count": decimal(count),
},
)