dita_v2: VenueTelemetrySnapshot + Zinc venue plane (4th shm partition) + local Rust target dir

Upstream-canonical commit of the ~366 lines that sat uncommitted in the working
tree (surveyed in DITAV2_SOA_SURVEY_20260702). Venue telemetry published at every
venue boundary (submit/cancel/reconcile) over the zinc venue_region; SOA per
UV DITAv2 adjudication 2026-07-03.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-07-03 00:42:20 +02:00
parent 1db4802ee8
commit 9bef1f6b01
8 changed files with 1091 additions and 8 deletions

View File

@@ -32,6 +32,7 @@ from .contracts import (
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
VenueTelemetrySnapshot,
)
from .journal import ClickHouseKernelJournal, KernelJournal, MemoryKernelJournal
from .rust_backend import ExecutionKernel
@@ -89,6 +90,7 @@ __all__ = [
"VenueEventStatus",
"VenueOrder",
"VenueOrderStatus",
"VenueTelemetrySnapshot",
"ZincPlane",
"ZincControlPlane",
"build_position_state_row",

View File

@@ -27,6 +27,7 @@ from .contracts import (
KernelEventKind,
KernelIntent,
TradeSide,
VenueTelemetrySnapshot,
VenueEvent,
VenueEventStatus,
VenueOrder,
@@ -226,7 +227,7 @@ class BingxVenueAdapter(VenueAdapter):
)
return cls._EXECUTOR
def __init__(self, backend: Any | None = None, *, config: Any | None = None) -> None:
def __init__(self, backend: Any | None = None, *, config: Any | None = None, zinc_plane: Any | None = None) -> None:
if backend is None:
if config is None:
raise ValueError("BingxVenueAdapter requires a backend or config")
@@ -234,6 +235,7 @@ class BingxVenueAdapter(VenueAdapter):
backend = BingxDirectExecutionAdapter(config)
self.backend = backend
self._telemetry_plane = zinc_plane
self._event_seq = itertools.count(1)
# Thread-safe snapshot cache — reads from a snapshot may arrive from
# the kernel thread while _backend_snapshot writes from the pool thread.
@@ -279,6 +281,73 @@ class BingxVenueAdapter(VenueAdapter):
except Exception:
pass
def set_telemetry_plane(self, zinc_plane: Any | None) -> None:
self._telemetry_plane = zinc_plane
def _publish_telemetry(
self,
*,
phase: str,
status: str,
intent: KernelIntent | None = None,
order: VenueOrder | None = None,
endpoint: str = "",
method: str = "",
message: str = "",
retry_after_ms: int = 0,
venue_order_status: str = "",
venue_event_kind: str = "",
details: dict[str, Any] | None = None,
) -> None:
plane = self._telemetry_plane
publish = getattr(plane, "publish_venue", None) if plane is not None else None
if publish is None:
return
slot_id = 0
trade_id = ""
asset = ""
side = TradeSide.FLAT
action = ""
intent_id = ""
if intent is not None:
slot_id = int(getattr(intent, "slot_id", 0) or 0)
trade_id = str(getattr(intent, "trade_id", "") or "")
asset = str(getattr(intent, "asset", "") or "")
side = getattr(intent, "side", TradeSide.FLAT)
action = str(getattr(intent, "action", "") or "")
intent_id = str(getattr(intent, "intent_id", "") or "")
if order is not None:
slot_id = int(order.metadata.get("slot_id", slot_id) or slot_id)
trade_id = str(order.internal_trade_id or trade_id)
asset = str(order.metadata.get("asset") or asset)
side = order.side or side
try:
publish(
VenueTelemetrySnapshot(
phase=phase,
status=status,
venue="bingx",
endpoint=endpoint,
method=method,
intent_id=intent_id,
trade_id=trade_id,
slot_id=slot_id,
asset=asset,
side=side,
action=action,
order_id=str(getattr(order, "venue_order_id", "") or ""),
client_order_id=str(getattr(order, "venue_client_id", "") or ""),
venue_order_status=venue_order_status,
venue_event_kind=venue_event_kind,
message=message,
retry_after_ms=int(retry_after_ms or 0),
timestamp=datetime.now(timezone.utc),
details=dict(details or {}),
)
)
except Exception:
pass
def _call_backend(self, method_name: str, *args: Any, **kwargs: Any) -> Any:
method = getattr(self.backend, method_name, None)
if method is None:
@@ -368,18 +437,37 @@ class BingxVenueAdapter(VenueAdapter):
was fixed for submit via submit_async. This version awaits backend.cancel()
directly in the caller's (main) event loop.
"""
self._publish_telemetry(
phase="cancel:start",
status="REQUESTED",
order=order,
endpoint="/openApi/swap/v2/trade/order",
method="DELETE",
message=reason,
details={"asset": str(order.metadata.get("asset") or "")},
)
cancel_fn = getattr(self.backend, "cancel", None)
if cancel_fn is not None:
response = await cancel_fn(order, reason=reason)
else:
response = None
return self._events_from_cancel(order, response, None, None, reason=reason)
events = self._events_from_cancel(order, response, None, None, reason=reason)
return events
def cancel(self, order: VenueOrder, *, reason: str = "") -> List[VenueEvent]:
# _events_from_cancel never reads before/after — snapshots are dead weight.
# NOTE: if backend.cancel is async (BingxDirectExecutionAdapter), this sync
# path goes through the thread-pool and will deadlock in a running event loop.
# Use cancel_async() from async contexts (process_intent_async already does).
self._publish_telemetry(
phase="cancel:start",
status="REQUESTED",
order=order,
endpoint="/openApi/swap/v2/trade/order",
method="DELETE",
message=reason,
details={"asset": str(order.metadata.get("asset") or "")},
)
response = None
if hasattr(self.backend, "cancel"):
response = self._call_backend("cancel", order, reason=reason)
@@ -410,7 +498,8 @@ class BingxVenueAdapter(VenueAdapter):
except BingxHttpError as exc:
# W10: map HTTP error class to status — 429/5xx are transient, 4xx are real rejections
response = {"status": _http_error_status(str(exc)), "msg": str(exc), "orderId": order.venue_order_id, "clientOrderId": order.venue_client_id}
return self._events_from_cancel(order, response, None, None, reason=reason)
events = self._events_from_cancel(order, response, None, None, reason=reason)
return events
def open_orders(self) -> List[VenueOrder]:
# Use backend._state (populated by await backend.connect()) rather than
@@ -458,6 +547,13 @@ class BingxVenueAdapter(VenueAdapter):
# entirely: the FSM stayed fill-blind (slot size 0 in ENTRY_WORKING),
# the DecisionEngine saw "no position", and re-entered → the live
# double-entries at 15:20 and 17:24 UTC.
self._publish_telemetry(
phase="reconcile:start",
status="REQUESTED",
endpoint="/openApi/swap/v2/trade/openOrders",
method="GET",
details={"include_history": True},
)
recon_symbol = None
kernel = getattr(self, "_kernel_ref", None)
if kernel is not None:
@@ -474,13 +570,60 @@ class BingxVenueAdapter(VenueAdapter):
except Exception as exc:
import logging as _log
_log.getLogger(__name__).warning("reconcile: refresh_state failed: %s", exc)
self._publish_telemetry(
phase="reconcile:error",
status="ERROR",
endpoint="/openApi/swap/v2/trade/openOrders",
method="GET",
message=str(exc),
details={"symbol": recon_symbol or ""},
)
return []
self._publish_telemetry(
phase="reconcile:done",
status="OK",
endpoint="/openApi/swap/v2/trade/openOrders",
method="GET",
details={
"symbol": recon_symbol or "",
"open_orders": len(getattr(snapshot, "open_orders", []) or []),
"positions": len(getattr(snapshot, "open_positions", {}) or {}),
"fills": len(getattr(snapshot, "all_fills", []) or []),
},
)
return self._events_from_snapshot(snapshot)
def submit(self, intent: KernelIntent) -> List[VenueEvent]:
# Snapshots dropped: receipt executedQty fields take precedence (same as submit_async)
self._publish_telemetry(
phase="submit:start",
status="REQUESTED",
intent=intent,
endpoint="/openApi/swap/v2/trade/order",
method="POST",
details={"action": intent.action.value, "order_type": str(getattr(intent, "order_type", "MARKET") or "MARKET")},
)
receipt = self._call_backend("submit_intent", self._legacy_intent(intent))
return self._events_from_submit(intent, receipt, None, None)
events = self._events_from_submit(intent, receipt, None, None)
ack_row = dict(getattr(receipt, "raw_ack", {}) or {})
self._publish_telemetry(
phase="submit:done",
status=str(getattr(receipt, "status", "") or _row_text(ack_row, "status", default="NEW")),
intent=intent,
endpoint="/openApi/swap/v2/trade/order",
method="POST",
message=_row_text(ack_row, "msg", "message", default=""),
order_id=_row_text(ack_row, "orderId", "orderID", default=str(getattr(receipt, "order_id", "") or "")),
client_order_id=_row_text(ack_row, "clientOrderID", "clientOrderId", default=str(getattr(receipt, "client_order_id", "") or intent.intent_id)),
venue_order_status=str(getattr(receipt, "status", "") or _row_text(ack_row, "status", default="")),
venue_event_kind=events[1].kind.value if len(events) > 1 else events[0].kind.value,
details={
"filled_size": float((events[1].filled_size if len(events) > 1 else events[0].filled_size) or 0.0),
"event_count": len(events),
"asset": intent.asset,
},
)
return events
async def submit_async(self, intent: KernelIntent) -> List[VenueEvent]:
"""Async submit — runs in the caller's event loop, no thread-pool deadlock.
@@ -495,8 +638,35 @@ class BingxVenueAdapter(VenueAdapter):
Passing None for snapshots makes _filled_size_from_snapshots return 0.0
(a safe fallback; the receipt fields take precedence).
"""
self._publish_telemetry(
phase="submit:start",
status="REQUESTED",
intent=intent,
endpoint="/openApi/swap/v2/trade/order",
method="POST",
details={"action": intent.action.value, "order_type": str(getattr(intent, "order_type", "MARKET") or "MARKET")},
)
receipt = await self.backend.submit_intent(self._legacy_intent(intent))
return self._events_from_submit(intent, receipt, None, None)
events = self._events_from_submit(intent, receipt, None, None)
ack_row = dict(getattr(receipt, "raw_ack", {}) or {})
self._publish_telemetry(
phase="submit:done",
status=str(getattr(receipt, "status", "") or _row_text(ack_row, "status", default="NEW")),
intent=intent,
endpoint="/openApi/swap/v2/trade/order",
method="POST",
message=_row_text(ack_row, "msg", "message", default=""),
order_id=_row_text(ack_row, "orderId", "orderID", default=str(getattr(receipt, "order_id", "") or "")),
client_order_id=_row_text(ack_row, "clientOrderID", "clientOrderId", default=str(getattr(receipt, "client_order_id", "") or intent.intent_id)),
venue_order_status=str(getattr(receipt, "status", "") or _row_text(ack_row, "status", default="")),
venue_event_kind=events[1].kind.value if len(events) > 1 else events[0].kind.value,
details={
"filled_size": float((events[1].filled_size if len(events) > 1 else events[0].filled_size) or 0.0),
"event_count": len(events),
"asset": intent.asset,
},
)
return events
def _events_from_submit(self, intent: KernelIntent, receipt: Any, before, after) -> List[VenueEvent]: # noqa: ANN001
ack_row = dict(getattr(receipt, "raw_ack", {}) or {})
@@ -612,6 +782,18 @@ class BingxVenueAdapter(VenueAdapter):
raw = response if isinstance(response, dict) else {}
status = _normalize_status(_row_text(raw, "status", default="CANCELED"))
if status in {"RATE_LIMITED", "THROTTLED"}:
self._publish_telemetry(
phase="cancel:done",
status=status,
order=order,
endpoint="/openApi/swap/v2/trade/order",
method="DELETE",
message=reason or _row_text(raw, "msg", "message", default="BINGX_RATE_LIMITED"),
venue_order_status=VenueEventStatus.RATE_LIMITED.value,
venue_event_kind=KernelEventKind.RATE_LIMITED.value,
retry_after_ms=_rate_limit_retry_after_ms(raw),
details={"order_status": status, "asset": str(order.metadata.get("asset") or "")},
)
return [
VenueEvent(
timestamp=datetime.now(timezone.utc),
@@ -637,6 +819,18 @@ class BingxVenueAdapter(VenueAdapter):
kind = KernelEventKind.CANCEL_ACK if event_status == VenueEventStatus.CANCELED else KernelEventKind.CANCEL_REJECT
if event_status == VenueEventStatus.CANCELED_REJECTED:
kind = KernelEventKind.CANCEL_REJECT
self._publish_telemetry(
phase="cancel:done",
status=status or event_status.value,
order=order,
endpoint="/openApi/swap/v2/trade/order",
method="DELETE",
message=reason or _row_text(raw, "msg", "message", default=""),
venue_order_status=event_status.value,
venue_event_kind=kind.value,
retry_after_ms=_rate_limit_retry_after_ms(raw),
details={"order_status": status, "asset": str(order.metadata.get("asset") or "")},
)
return [
VenueEvent(
timestamp=datetime.now(timezone.utc),

View File

@@ -144,6 +144,54 @@ class VenueOrder:
return max(0.0, float(self.intended_size) - float(self.filled_size))
@dataclass(frozen=True)
class VenueTelemetrySnapshot:
"""Shared-memory surface for venue-side call boundaries and state."""
phase: str = "idle"
status: str = "IDLE"
venue: str = "bingx"
endpoint: str = ""
method: str = ""
intent_id: str = ""
trade_id: str = ""
slot_id: int = 0
asset: str = ""
side: TradeSide = TradeSide.FLAT
action: str = ""
order_id: str = ""
client_order_id: str = ""
venue_order_status: str = ""
venue_event_kind: str = ""
message: str = ""
retry_after_ms: int = 0
timestamp: Optional[datetime] = None
details: Dict[str, Any] = field(default_factory=dict)
def as_dict(self) -> Dict[str, Any]:
return {
"phase": self.phase,
"status": self.status,
"venue": self.venue,
"endpoint": self.endpoint,
"method": self.method,
"intent_id": self.intent_id,
"trade_id": self.trade_id,
"slot_id": int(self.slot_id or 0),
"asset": self.asset,
"side": self.side.value if hasattr(self.side, "value") else str(self.side),
"action": self.action,
"order_id": self.order_id,
"client_order_id": self.client_order_id,
"venue_order_status": self.venue_order_status,
"venue_event_kind": self.venue_event_kind,
"message": self.message,
"retry_after_ms": int(self.retry_after_ms or 0),
"timestamp": self.timestamp.isoformat() if hasattr(self.timestamp, "isoformat") else None,
"details": dict(self.details),
}
@dataclass
class TradeSlot:
"""A single execution slot managed by the v2 kernel."""

View File

@@ -245,9 +245,15 @@ def _build_venue(
mock_scenario: Optional[MockVenueScenario] = None,
bingx_config: Optional[BingxExecClientConfig] = None,
bingx_backend: Optional[Any] = None,
zinc_plane: Optional[ZincPlane] = None,
venue: Optional[VenueAdapter] = None,
) -> VenueAdapter:
if venue is not None:
if zinc_plane is not None and hasattr(venue, "set_telemetry_plane"):
try:
venue.set_telemetry_plane(zinc_plane)
except Exception:
pass
return venue
resolved_mode = venue_mode or _resolve_venue_mode()
if resolved_mode is LauncherVenueMode.BINGX:
@@ -256,7 +262,7 @@ def _build_venue(
from prod.clean_arch.adapters.bingx_direct import BingxDirectExecutionAdapter
backend = BingxDirectExecutionAdapter(bingx_config or build_bingx_exec_client_config())
return BingxVenueAdapter(backend=backend)
return BingxVenueAdapter(backend=backend, zinc_plane=zinc_plane)
return MockVenueAdapter(mock_scenario)
@@ -342,6 +348,7 @@ def build_launcher_bundle(
mock_scenario=mock_scenario,
bingx_config=bingx_config,
bingx_backend=bingx_backend,
zinc_plane=active_zinc_plane,
venue=venue,
)
kernel = ExecutionKernel(

View File

@@ -16,7 +16,7 @@ import struct
import sys
import threading
from .contracts import KernelIntent, TradeSide, TradeSlot, TradeStage, VenueOrder, VenueOrderStatus
from .contracts import KernelIntent, TradeSide, TradeSlot, TradeStage, VenueOrder, VenueOrderStatus, VenueTelemetrySnapshot
from .control import KernelControlSnapshot
_ZINC_ADAPTER_PATH = Path(__file__).resolve().parents[3] / "zinc" / "adapters" / "python"
@@ -130,6 +130,42 @@ def _decode_packet(buf: memoryview) -> Dict[str, Any]:
return out
def _venue_from_payload(payload: Dict[str, Any]) -> VenueTelemetrySnapshot:
timestamp = payload.get("timestamp")
ts_value = None
if isinstance(timestamp, str) and timestamp:
try:
ts_value = datetime.fromisoformat(timestamp)
except Exception:
ts_value = None
try:
side = TradeSide(str(payload.get("side", TradeSide.FLAT.value)))
except Exception:
side = TradeSide.FLAT
details = payload.get("details", {})
return VenueTelemetrySnapshot(
phase=str(payload.get("phase", "idle")),
status=str(payload.get("status", "IDLE")),
venue=str(payload.get("venue", "bingx")),
endpoint=str(payload.get("endpoint", "")),
method=str(payload.get("method", "")),
intent_id=str(payload.get("intent_id", "")),
trade_id=str(payload.get("trade_id", "")),
slot_id=int(payload.get("slot_id", 0) or 0),
asset=str(payload.get("asset", "")),
side=side,
action=str(payload.get("action", "")),
order_id=str(payload.get("order_id", "")),
client_order_id=str(payload.get("client_order_id", "")),
venue_order_status=str(payload.get("venue_order_status", "")),
venue_event_kind=str(payload.get("venue_event_kind", "")),
message=str(payload.get("message", "")),
retry_after_ms=int(payload.get("retry_after_ms", 0) or 0),
timestamp=ts_value,
details=dict(details) if isinstance(details, dict) else {},
)
class RealZincPlane:
"""Shared-memory Zinc plane used by the Python prototype."""
@@ -148,18 +184,22 @@ class RealZincPlane:
self.intent_name = f"{base}_intent"
self.state_name = f"{base}_state"
self.control_name = f"{base}_control"
self.venue_name = f"{base}_venue"
self._intent_seq = 0
self._state_seq = 0
self._control_seq = 0
self._venue_seq = 0
self._lock = threading.Lock()
self._slot_cache: Dict[int, TradeSlot] = {i: TradeSlot(slot_id=i) for i in range(int(slot_count))}
self._slot_count = int(slot_count)
self._intent_cache: List[Dict[str, Any]] = []
self._control_cache = KernelControlSnapshot()
self._venue_cache = VenueTelemetrySnapshot()
if create:
self.intent_region = SharedRegion.create(self.intent_name, intent_capacity)
self.state_region = SharedRegion.create(self.state_name, state_capacity)
self.control_region = SharedRegion.create(self.control_name, control_capacity)
self.venue_region = SharedRegion.create(self.venue_name, control_capacity)
self._write_region(self.control_region, self._control_seq, {"control": self._control_cache.as_dict()})
self._write_region(
self.state_region,
@@ -167,13 +207,16 @@ class RealZincPlane:
{"slots": [self._slot_cache[key].to_dict() for key in range(self._slot_count)]},
)
self._write_region(self.intent_region, self._intent_seq, {"items": []})
self._write_region(self.venue_region, self._venue_seq, {"venue": self._venue_cache.as_dict()})
else:
self.intent_region = SharedRegion.open(self.intent_name)
self.state_region = SharedRegion.open(self.state_name)
self.control_region = SharedRegion.open(self.control_name)
self.venue_region = SharedRegion.open(self.venue_name)
control_payload = _decode_packet(self.control_region.as_buffer())
state_payload = _decode_packet(self.state_region.as_buffer())
intent_payload = _decode_packet(self.intent_region.as_buffer())
venue_payload = _decode_packet(self.venue_region.as_buffer())
if isinstance(control_payload.get("control"), dict):
self._control_cache = KernelControlSnapshot(**control_payload["control"])
if isinstance(state_payload.get("slots"), list):
@@ -183,11 +226,14 @@ class RealZincPlane:
self._slot_cache[int(slot.slot_id)] = slot
if isinstance(intent_payload.get("items"), list):
self._intent_cache = list(intent_payload["items"])
if isinstance(venue_payload.get("venue"), dict):
self._venue_cache = _venue_from_payload(venue_payload["venue"])
def close(self) -> None:
self.intent_region.close()
self.state_region.close()
self.control_region.close()
self.venue_region.close()
def publish_intent(self, intent: KernelIntent) -> None:
with self._lock:
@@ -246,6 +292,26 @@ class RealZincPlane:
def notify_control(self) -> None:
self.control_region.notify()
def publish_venue(self, telemetry: VenueTelemetrySnapshot) -> None:
with self._lock:
self._venue_seq += 1
self._venue_cache = telemetry
self._write_region(self.venue_region, self._venue_seq, {"venue": telemetry.as_dict()})
def read_venue(self) -> VenueTelemetrySnapshot:
payload = _decode_packet(self.venue_region.as_buffer())
venue = payload.get("venue") if isinstance(payload, dict) else None
if not isinstance(venue, dict):
return self._venue_cache
self._venue_cache = _venue_from_payload(venue)
return self._venue_cache
def wait_on_venue(self, timeout_ms: int = 1000) -> bool:
return bool(self.venue_region.wait(timeout_ms))
def notify_venue(self) -> None:
self.venue_region.notify()
def wait_on_intent(self, timeout_ms: int = 1000) -> bool:
return bool(self.intent_region.wait(timeout_ms))

View File

@@ -107,6 +107,9 @@ def _crate_dir() -> Path:
return Path(__file__).resolve().with_name("_rust_kernel")
_LOCAL_TARGET_DIR = Path("/root/.cargo/dita_v2_target")
def _library_path() -> Path:
if sys.platform == "darwin":
name = "libdita_v2_kernel.dylib"
@@ -114,6 +117,9 @@ def _library_path() -> Path:
name = "dita_v2_kernel.dll"
else:
name = "libdita_v2_kernel.so"
local = _LOCAL_TARGET_DIR / "release" / name
if local.exists():
return local
return _crate_dir() / "target" / "release" / name
@@ -121,10 +127,13 @@ def _build_library() -> None:
crate_dir = _crate_dir()
if not crate_dir.exists():
raise FileNotFoundError(f"Missing Rust kernel crate: {crate_dir}")
_LOCAL_TARGET_DIR.mkdir(parents=True, exist_ok=True)
env = {**os.environ, "CARGO_TARGET_DIR": str(_LOCAL_TARGET_DIR)}
subprocess.run(
["cargo", "build", "--release", "--manifest-path", str(crate_dir / "Cargo.toml")],
cwd=_repo_root(),
check=True,
env=env,
)

View File

@@ -12,7 +12,7 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Protocol
import threading
import time
from .contracts import KernelIntent, TradeSlot
from .contracts import KernelIntent, TradeSlot, VenueTelemetrySnapshot
from .control import KernelControlSnapshot
@@ -52,6 +52,18 @@ class ZincPlane(Protocol):
def notify_control(self) -> None:
...
def publish_venue(self, telemetry: VenueTelemetrySnapshot) -> None:
...
def read_venue(self) -> VenueTelemetrySnapshot:
...
def wait_on_venue(self, timeout_ms: int = 1000) -> bool:
...
def notify_venue(self) -> None:
...
@dataclass
class InMemoryZincPlane:
@@ -60,12 +72,15 @@ class InMemoryZincPlane:
intent_region: List[KernelIntent] = field(default_factory=list)
state_region: Dict[int, TradeSlot] = field(default_factory=dict)
control_region: Optional[KernelControlSnapshot] = None
venue_region: VenueTelemetrySnapshot = field(default_factory=VenueTelemetrySnapshot)
_intent_seq: int = field(default=0, init=False, repr=False)
_state_seq: int = field(default=0, init=False, repr=False)
_control_seq: int = field(default=0, init=False, repr=False)
_venue_seq: int = field(default=0, init=False, repr=False)
_intent_observed_seq: int = field(default=0, init=False, repr=False)
_state_observed_seq: int = field(default=0, init=False, repr=False)
_control_observed_seq: int = field(default=0, init=False, repr=False)
_venue_observed_seq: int = field(default=0, init=False, repr=False)
_signal: threading.Condition = field(default_factory=threading.Condition, init=False, repr=False)
def publish_intent(self, intent: KernelIntent) -> None:
@@ -118,6 +133,23 @@ class InMemoryZincPlane:
self._control_seq += 1
self._signal.notify_all()
def publish_venue(self, telemetry: VenueTelemetrySnapshot) -> None:
with self._signal:
self.venue_region = telemetry
self._venue_seq += 1
self._signal.notify_all()
def read_venue(self) -> VenueTelemetrySnapshot:
return self.venue_region
def wait_on_venue(self, timeout_ms: int = 1000) -> bool:
return self._wait_for_change("_venue_seq", "_venue_observed_seq", timeout_ms)
def notify_venue(self) -> None:
with self._signal:
self._venue_seq += 1
self._signal.notify_all()
def _wait_for_change(self, seq_attr: str, observed_attr: str, timeout_ms: int) -> bool:
timeout_s = None if timeout_ms is None or timeout_ms < 0 else max(0.0, timeout_ms / 1000.0)
deadline = None if timeout_s is None else time.monotonic() + timeout_s