wip(k1): checkpoint connection recovery rewrite
Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
This commit is contained in:
@@ -7,18 +7,24 @@ import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from k1link.compute.live_perception import (
|
||||
LivePerceptionIngress,
|
||||
encode_live_perception_result,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.live_perception_shadow import (
|
||||
build_live_perception_result_receiver,
|
||||
build_live_perception_shadow_router,
|
||||
ensure_live_shadow_token,
|
||||
)
|
||||
|
||||
|
||||
def test_shadow_token_is_stable_and_private(tmp_path: Path) -> None:
|
||||
def test_shadow_token_is_stable_and_private(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.delenv("MISSIONCORE_DATA_DIR", raising=False)
|
||||
path, token = ensure_live_shadow_token(tmp_path)
|
||||
repeated_path, repeated_token = ensure_live_shadow_token(tmp_path)
|
||||
|
||||
@@ -36,9 +42,7 @@ def test_shadow_router_exposes_only_the_exclusive_binary_stream() -> None:
|
||||
bearer_token="x" * 43,
|
||||
)
|
||||
assert len(router.routes) == 1
|
||||
assert router.routes[0].path == (
|
||||
"/api/v1/device-plugins/test-plugin/live-perception-shadow"
|
||||
)
|
||||
assert router.routes[0].path == ("/api/v1/device-plugins/test-plugin/live-perception-shadow")
|
||||
|
||||
|
||||
def test_shadow_router_accepts_only_validated_diagnostic_results_back() -> None:
|
||||
@@ -59,6 +63,8 @@ def test_shadow_router_accepts_only_validated_diagnostic_results_back() -> None:
|
||||
)
|
||||
ingress.begin_session("session-1")
|
||||
encoded = encode_live_perception_result(
|
||||
session_id="session-1",
|
||||
session_generation=1,
|
||||
frame_index=0,
|
||||
source_frame_index=0,
|
||||
session_seconds=0.0,
|
||||
@@ -100,6 +106,95 @@ def test_shadow_router_accepts_only_validated_diagnostic_results_back() -> None:
|
||||
assert received == [encoded]
|
||||
|
||||
|
||||
def test_shadow_result_receiver_rejects_previous_acquisition_generation() -> None:
|
||||
ingress = LivePerceptionIngress()
|
||||
published: list[tuple[str, int]] = []
|
||||
receiver = build_live_perception_result_receiver(
|
||||
ingress,
|
||||
lambda frame: not published.append((frame.session_id, frame.session_generation)),
|
||||
)
|
||||
ingress.begin_session("session-1")
|
||||
encoded = encode_live_perception_result(
|
||||
session_id="session-1",
|
||||
session_generation=1,
|
||||
frame_index=0,
|
||||
source_frame_index=0,
|
||||
session_seconds=0.0,
|
||||
captured_at_epoch_ns=1,
|
||||
image_jpeg=bytes.fromhex("ffd878ffd9"),
|
||||
segmentation_mask=None,
|
||||
objects=[],
|
||||
delivery={"health": "healthy"},
|
||||
)
|
||||
ingress.end_session("session-1")
|
||||
ingress.begin_session("session-2")
|
||||
|
||||
assert receiver(encoded) is False
|
||||
assert published == []
|
||||
snapshot = ingress.snapshot()
|
||||
assert snapshot["results_accepted"] == 0
|
||||
assert snapshot["results_rejected_stale"] == 1
|
||||
assert snapshot["results_rejected_receiver"] == 0
|
||||
|
||||
|
||||
def test_shadow_router_closes_a_worker_that_publishes_for_previous_session() -> None:
|
||||
ingress = LivePerceptionIngress()
|
||||
published: list[int] = []
|
||||
receiver = build_live_perception_result_receiver(
|
||||
ingress,
|
||||
lambda frame: not published.append(frame.frame_index),
|
||||
)
|
||||
router = build_live_perception_shadow_router(
|
||||
ingress,
|
||||
"test-plugin",
|
||||
bearer_token="x" * 43,
|
||||
result_receiver=receiver,
|
||||
)
|
||||
ingress.begin_session("session-1")
|
||||
stale = encode_live_perception_result(
|
||||
session_id="session-1",
|
||||
session_generation=1,
|
||||
frame_index=7,
|
||||
source_frame_index=7,
|
||||
session_seconds=0.0,
|
||||
captured_at_epoch_ns=1,
|
||||
image_jpeg=bytes.fromhex("ffd878ffd9"),
|
||||
segmentation_mask=None,
|
||||
objects=[],
|
||||
delivery={"health": "healthy"},
|
||||
)
|
||||
ingress.end_session("session-1")
|
||||
ingress.begin_session("session-2")
|
||||
|
||||
class StaleResultWebSocket:
|
||||
def __init__(self) -> None:
|
||||
self.headers = {"authorization": f"Bearer {'x' * 43}"}
|
||||
self.closed: list[dict[str, object]] = []
|
||||
|
||||
async def accept(self) -> None:
|
||||
return
|
||||
|
||||
async def receive(self) -> dict[str, object]:
|
||||
await asyncio.sleep(0)
|
||||
return {"type": "websocket.receive", "bytes": stale}
|
||||
|
||||
async def send_bytes(self, _payload: bytes) -> None:
|
||||
return
|
||||
|
||||
async def close(self, **values: object) -> None:
|
||||
self.closed.append(values)
|
||||
|
||||
websocket = StaleResultWebSocket()
|
||||
asyncio.run(router.routes[0].endpoint(websocket)) # type: ignore[attr-defined]
|
||||
|
||||
assert published == []
|
||||
assert any(
|
||||
item.get("code") == 1008 and item.get("reason") == "Shadow result session is stale"
|
||||
for item in websocket.closed
|
||||
)
|
||||
assert ingress.snapshot()["results_rejected_stale"] == 1
|
||||
|
||||
|
||||
def test_shadow_router_does_not_discard_ingress_while_receiving_results() -> None:
|
||||
class SlowIngress(LivePerceptionIngress):
|
||||
def take_next(
|
||||
@@ -135,9 +230,7 @@ def test_shadow_router_does_not_discard_ingress_while_receiving_results() -> Non
|
||||
payload=modality.encode(),
|
||||
)
|
||||
ingress.end_session("session-1")
|
||||
expected_events = sum(
|
||||
int(queue["depth"]) for queue in ingress.snapshot()["queues"].values()
|
||||
)
|
||||
expected_events = sum(int(queue["depth"]) for queue in ingress.snapshot()["queues"].values())
|
||||
|
||||
class DuplexFakeWebSocket:
|
||||
def __init__(self) -> None:
|
||||
@@ -172,6 +265,4 @@ def test_shadow_router_does_not_discard_ingress_while_receiving_results() -> Non
|
||||
assert len(received) == 8
|
||||
assert len(websocket.sent) == expected_events
|
||||
snapshot = ingress.snapshot()
|
||||
assert sum(
|
||||
int(queue["consumed"]) for queue in snapshot["queues"].values()
|
||||
) == expected_events
|
||||
assert sum(int(queue["consumed"]) for queue in snapshot["queues"].values()) == expected_events
|
||||
|
||||
Reference in New Issue
Block a user