feat(perception): add scoped stream control and bounded clock observations
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
"""CPU-only clock/grant route evidence; NOT a model/freshness qualification.
|
||||
|
||||
One bootstrap ticket/certificate copied via authenticated SSH, subsequent data
|
||||
grants delivered only by TLS Poll. Source events/resume proof remain synthetic;
|
||||
real monotonic clock observations use explicit CONDITIONAL error/rate budgets.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import resource
|
||||
import secrets
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import asdict
|
||||
from datetime import UTC, datetime
|
||||
from hashlib import sha256
|
||||
from pathlib import Path
|
||||
|
||||
import grpc
|
||||
from grpc_transport_probe import document, identity, until
|
||||
|
||||
from k1link.compute.live_perception import LiveIngressEvent
|
||||
from k1link.perception.realtime_contract import StreamStart
|
||||
from k1link.perception.streaming_clock import ClockMappingError, ClockWindow
|
||||
from k1link.perception.streaming_continuity import ResumeEvidence
|
||||
from k1link.perception.streaming_control_grpc import (
|
||||
ControlTicket,
|
||||
StreamControlClient,
|
||||
StreamControlEndpoint,
|
||||
)
|
||||
from k1link.perception.streaming_grpc import GrpcStreamClient, GrpcStreamEndpoint
|
||||
from k1link.perception.streaming_lifecycle import StreamingLifecycle
|
||||
from k1link.perception.streaming_queue import StreamMailbox
|
||||
|
||||
|
||||
async def server(root):
|
||||
private = root / "private"
|
||||
private.mkdir(mode=0o700, exist_ok=True)
|
||||
cert, key, bootstrap = [private / x for x in ("cert.pem", "key.pem", "bootstrap.json")]
|
||||
subprocess.run(
|
||||
[
|
||||
"openssl",
|
||||
"req",
|
||||
"-x509",
|
||||
"-newkey",
|
||||
"rsa:2048",
|
||||
"-nodes",
|
||||
"-days",
|
||||
"1",
|
||||
"-subj",
|
||||
"/CN=localhost",
|
||||
"-keyout",
|
||||
str(key),
|
||||
"-out",
|
||||
str(cert),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
)
|
||||
run = StreamingLifecycle(
|
||||
identity(),
|
||||
root / "lease",
|
||||
StreamMailbox(),
|
||||
threading.Event(),
|
||||
recover_input=True,
|
||||
source_clock_ns=lambda: 1_000_000_000,
|
||||
)
|
||||
resident = run.spawn(
|
||||
lambda: subprocess.Popen(
|
||||
[sys.executable, "-c", "import time; time.sleep(180)"],
|
||||
start_new_session=True,
|
||||
)
|
||||
)
|
||||
run.ready()
|
||||
pulse_stop = threading.Event()
|
||||
|
||||
def pulse():
|
||||
while not pulse_stop.wait(0.1):
|
||||
run.renew(identity())
|
||||
|
||||
pulse_thread = threading.Thread(target=pulse, daemon=True)
|
||||
pulse_thread.start()
|
||||
received = []
|
||||
|
||||
def consume(event):
|
||||
item = {
|
||||
"sequence": event.ingress_sequence,
|
||||
"sha256": sha256(event.payload).hexdigest(),
|
||||
"bytes": len(event.payload),
|
||||
"resident_pid": resident.pid,
|
||||
}
|
||||
received.append(item)
|
||||
if len(received) > 8:
|
||||
raise ValueError("bounded count exceeded")
|
||||
endpoint.publish(
|
||||
run.continuity.epoch, event.ingress_sequence, json.dumps(item, sort_keys=True).encode()
|
||||
)
|
||||
|
||||
endpoint = GrpcStreamEndpoint(run, consume, lambda _: None)
|
||||
pending = [endpoint.issue(run.continuity.epoch, "synthetic-capture", 1)]
|
||||
ticket = ControlTicket(identity(), secrets.token_hex(32))
|
||||
clock_id = "worker-process-" + secrets.token_hex(12)
|
||||
control = StreamControlEndpoint(
|
||||
ticket,
|
||||
lambda: pending[0] if endpoint.grant is not None else None,
|
||||
clock_id=clock_id,
|
||||
)
|
||||
report = {
|
||||
"utc": datetime.now(UTC).isoformat(),
|
||||
"started_monotonic_ns": str(time.monotonic_ns()),
|
||||
"model_count": 0,
|
||||
"synthetic_source_and_resume": True,
|
||||
"real_time_qualified": False,
|
||||
"actuation_allowed": False,
|
||||
"received": received,
|
||||
"clock_id": clock_id,
|
||||
"grpc_version": grpc.__version__,
|
||||
}
|
||||
rpc = None
|
||||
try:
|
||||
rpc, _ = await endpoint.serve(
|
||||
"0.0.0.0:50061",
|
||||
certificate=cert.read_bytes(),
|
||||
private_key=key.read_bytes(),
|
||||
control_handlers=(control.handler(),),
|
||||
)
|
||||
document(
|
||||
bootstrap,
|
||||
{
|
||||
"activation": identity().to_dict(),
|
||||
"token": ticket.token,
|
||||
"remote_clock_id": clock_id,
|
||||
},
|
||||
)
|
||||
bootstrap.chmod(0o600)
|
||||
await until(lambda: len(received) == 4 and endpoint.active is None, seconds=90)
|
||||
report["first_transport"] = endpoint.last
|
||||
report["before_gap"] = run.snapshot()
|
||||
began = time.monotonic_ns()
|
||||
await asyncio.sleep(2.2)
|
||||
report["gap_ns"] = str(time.monotonic_ns() - began)
|
||||
assert resident.poll() is None and run.continuity.phase == "waiting"
|
||||
report["after_gap"] = run.snapshot()
|
||||
epoch = run.begin_input(identity())
|
||||
run.resume_input(epoch, ResumeEvidence(*([1_000_000_000] * 4), True), lambda: None)
|
||||
pending[0] = endpoint.issue(epoch, "synthetic-capture", 1)
|
||||
await until(lambda: len(received) == 8 and run.mailbox.done, seconds=30)
|
||||
endpoint.finish_results(epoch)
|
||||
await until(lambda: endpoint.active is None)
|
||||
report["second_transport"] = endpoint.last
|
||||
report["passed"] = resident.poll() is None
|
||||
report["peak_mailbox_bytes"] = run.mailbox.peak_bytes
|
||||
finally:
|
||||
if rpc:
|
||||
await rpc.stop(0)
|
||||
pulse_stop.set()
|
||||
pulse_thread.join(1)
|
||||
report["closed"] = run.close()
|
||||
report["resident_reaped"] = resident.poll() is not None
|
||||
report["final_mailbox_bytes"] = run.mailbox.bytes
|
||||
report["control_accepted"] = control.accepted
|
||||
report["control_rejected"] = control.rejected
|
||||
report["rss_max_kib"] = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
||||
report["finished_monotonic_ns"] = str(time.monotonic_ns())
|
||||
document(root / "server-report.json", report)
|
||||
for path in (key, bootstrap):
|
||||
path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
async def client(root, target):
|
||||
bootstrap = root / "bootstrap.json"
|
||||
value = json.loads(bootstrap.read_text())
|
||||
ticket = ControlTicket(StreamStart.from_dict(value["activation"]), value["token"])
|
||||
local_clock_id = "mac-process-" + secrets.token_hex(12)
|
||||
control = StreamControlClient(
|
||||
target, (root / "cert.pem").read_bytes(), ticket, clock_id=local_clock_id
|
||||
)
|
||||
mapping = ClockWindow(
|
||||
local_clock_id,
|
||||
value["remote_clock_id"],
|
||||
rate_ppm=500,
|
||||
timestamp_error_ns=50_000,
|
||||
maximum_age_ns=2_000_000_000,
|
||||
)
|
||||
rows, grants, replies, errors = [], [], [], []
|
||||
report = {
|
||||
"utc": datetime.now(UTC).isoformat(),
|
||||
"started_monotonic_ns": str(time.monotonic_ns()),
|
||||
"schema_version": "missioncore.grpc-control-clock-probe/v1",
|
||||
"observations": rows,
|
||||
"grants": grants,
|
||||
"replies": replies,
|
||||
"errors": errors,
|
||||
"model_count": 0,
|
||||
"synthetic_source_and_resume": True,
|
||||
"real_time_qualified": False,
|
||||
"one_way_frame_age_measured": False,
|
||||
"actuation_allowed": False,
|
||||
"conditional_relative_rate_ppm": 500,
|
||||
"timestamp_error_budget_ns": "50000",
|
||||
"maximum_mapping_age_ns": "2000000000",
|
||||
"admission_uncertainty_ns": "5000000",
|
||||
"path": "TLS gRPC over existing Mac-Worker authenticated SSH/Tailscale route",
|
||||
}
|
||||
|
||||
async def poll(phase):
|
||||
for _ in range(4):
|
||||
try:
|
||||
probe, access = await control.poll()
|
||||
break
|
||||
except grpc.aio.AioRpcError as exc:
|
||||
errors.append({"phase": phase, "code": exc.code().name})
|
||||
await asyncio.sleep(0.1)
|
||||
else:
|
||||
raise RuntimeError("bounded control polling failed")
|
||||
bounds = mapping.add(probe)
|
||||
now = time.monotonic_ns()
|
||||
uncertainty = bounds.uncertainty_ns(now)
|
||||
raw = {k: str(v) if type(v) is int else v for k, v in asdict(probe).items()}
|
||||
rows.append(
|
||||
{
|
||||
"phase": phase,
|
||||
"probe": raw,
|
||||
"bounds": bounds.to_dict(),
|
||||
"evaluated_at_ns": str(now),
|
||||
"uncertainty_ns": str(uncertainty),
|
||||
"admitted_5ms": uncertainty <= 5_000_000,
|
||||
}
|
||||
)
|
||||
return access
|
||||
|
||||
try:
|
||||
previous = None
|
||||
for phase in (1, 2):
|
||||
if phase == 2:
|
||||
await asyncio.sleep(2.3)
|
||||
try:
|
||||
mapping.current(time.monotonic_ns())
|
||||
except ClockMappingError:
|
||||
report["mapping_expired_during_gap"] = True
|
||||
else:
|
||||
raise AssertionError("old clock mapping survived expiry")
|
||||
access = None
|
||||
for _ in range(32):
|
||||
candidate = await poll(phase)
|
||||
if candidate is not None:
|
||||
access = candidate
|
||||
await asyncio.sleep(0.05)
|
||||
assert access is not None and (previous is None or access.epoch != previous.epoch)
|
||||
assert previous is None or access.token != previous.token
|
||||
grants.append(
|
||||
{
|
||||
"phase": phase,
|
||||
"epoch": access.epoch.to_dict(),
|
||||
"token_changed": previous is not None,
|
||||
"delivery": "authenticated-control-poll",
|
||||
}
|
||||
)
|
||||
stream = GrpcStreamClient(target, (root / "cert.pem").read_bytes(), access)
|
||||
try:
|
||||
await stream.open()
|
||||
for sequence in range((phase - 1) * 4 + 1, phase * 4 + 1):
|
||||
payload = bytes([sequence]) * 32768
|
||||
event = LiveIngressEvent(
|
||||
sequence,
|
||||
"synthetic-capture",
|
||||
1,
|
||||
"lidar",
|
||||
"lidar",
|
||||
sequence,
|
||||
1_799_999_999_123_456_789,
|
||||
1_000_000_000,
|
||||
payload,
|
||||
)
|
||||
started = time.monotonic_ns()
|
||||
await stream.send(event)
|
||||
result = await stream.receive()
|
||||
elapsed = time.monotonic_ns() - started
|
||||
returned = json.loads(result[1])
|
||||
assert (
|
||||
result[0] == sequence and returned["sha256"] == sha256(payload).hexdigest()
|
||||
)
|
||||
replies.append({**returned, "round_trip_ns": str(elapsed)})
|
||||
assert await poll(phase) is None # Control still works during Exchange.
|
||||
await asyncio.sleep(0.05)
|
||||
if phase == 2:
|
||||
await stream.end()
|
||||
assert await stream.receive() is None
|
||||
finally:
|
||||
await stream.close()
|
||||
previous = access
|
||||
report["passed"] = True
|
||||
finally:
|
||||
await control.close()
|
||||
report["finished_monotonic_ns"] = str(time.monotonic_ns())
|
||||
document(root / "client-report.json", report)
|
||||
bootstrap.unlink(missing_ok=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("mode", choices=("server", "client"))
|
||||
parser.add_argument("root", type=Path)
|
||||
parser.add_argument("--target", default="localhost:18561")
|
||||
args = parser.parse_args()
|
||||
os.umask(0o077)
|
||||
asyncio.run(server(args.root) if args.mode == "server" else client(args.root, args.target))
|
||||
Reference in New Issue
Block a user