- I1 (Critical/Rust): apply_fill accumulated partial fills instead of overwriting. WS events carry lastFilledQty (incremental); previous code set slot.size = fill_size each time. Now accumulates via prev_filled. initial_size set from intended_size on first fill, not from fill amount. - G2 (Critical/Rust): into_c_string unwrap() panicked on any NUL byte in serialized JSON. Now sanitizes NUL bytes before CString construction; never panics. - G3 (Critical/Rust): EXIT intent transition hardcoded prev_state= POSITION_OPEN. Captured actual fsm_state before mutation so audit trail is accurate when EXIT is received from non-standard states. - I13 (High/Rust): stray venue event could reactivate a closed slot. Added explicit slot.closed guard in on_venue_event — returns TERMINAL_STATE with accepted=false before any FSM mutation. - I18 (High/Python): sys.path.insert(0, ...) in real_zinc_plane.py and real_control_plane.py gave Zinc adapter directory highest import priority. Changed to sys.path.append() so existing path entries take precedence. 35/35 offline tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
130 lines
4.3 KiB
Python
130 lines
4.3 KiB
Python
"""Real Zinc-backed control plane for DITAv2."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import struct
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Optional
|
|
|
|
from .control import BackendMode, ControlPlane, ControlUpdate, KernelControlSnapshot, KernelMode, KernelVerbosity
|
|
|
|
_ZINC_ADAPTER_PATH = Path(__file__).resolve().parents[3] / "zinc" / "adapters" / "python"
|
|
if _ZINC_ADAPTER_PATH.exists() and str(_ZINC_ADAPTER_PATH) not in sys.path:
|
|
sys.path.append(str(_ZINC_ADAPTER_PATH))
|
|
|
|
try: # pragma: no cover - exercised in integration tests
|
|
from zinc import SharedRegion
|
|
except Exception as exc: # pragma: no cover
|
|
SharedRegion = None # type: ignore[assignment]
|
|
_ZINC_IMPORT_ERROR = exc
|
|
else:
|
|
_ZINC_IMPORT_ERROR = None
|
|
|
|
|
|
class RealZincUnavailable(RuntimeError):
|
|
"""Raised when the Zinc Python adapter cannot be loaded."""
|
|
|
|
|
|
def require_real_zinc() -> None:
|
|
if SharedRegion is None:
|
|
raise RealZincUnavailable(str(_ZINC_IMPORT_ERROR))
|
|
|
|
|
|
def _json_default(value: Any) -> Any:
|
|
if hasattr(value, "value"):
|
|
return value.value
|
|
if hasattr(value, "isoformat"):
|
|
try:
|
|
return value.isoformat()
|
|
except Exception:
|
|
pass
|
|
if hasattr(value, "__dict__"):
|
|
return dict(vars(value))
|
|
raise TypeError(f"Unsupported value: {type(value)!r}")
|
|
|
|
|
|
def _encode_packet(seq: int, payload: Dict[str, Any]) -> bytes:
|
|
text = json.dumps(payload, sort_keys=True, ensure_ascii=False, default=_json_default, separators=(",", ":")).encode("utf-8")
|
|
return struct.pack("!QQ", int(seq), len(text)) + text
|
|
|
|
|
|
def _decode_packet(buf: memoryview) -> Dict[str, Any]:
|
|
if len(buf) < 16:
|
|
return {}
|
|
seq, size = struct.unpack_from("!QQ", buf, 0)
|
|
if size <= 0 or size > len(buf) - 16:
|
|
return {}
|
|
payload = bytes(buf[16 : 16 + size]).decode("utf-8")
|
|
out = json.loads(payload)
|
|
if isinstance(out, dict):
|
|
out["_seq"] = seq
|
|
return out
|
|
|
|
|
|
class RealZincControlPlane(ControlPlane):
|
|
"""Shared-memory Zinc-backed control plane."""
|
|
|
|
def __init__(self, *, prefix: str, create: bool = True) -> None:
|
|
require_real_zinc()
|
|
base = prefix.strip("/").replace("/", "_")
|
|
self.region_name = f"{base}_control"
|
|
self._seq = 0
|
|
self._snapshot = KernelControlSnapshot()
|
|
if create:
|
|
self.region = SharedRegion.create(self.region_name, 1 << 20)
|
|
self._write_region(self._seq, self._snapshot.as_dict())
|
|
else:
|
|
self.region = SharedRegion.open(self.region_name)
|
|
payload = _decode_packet(self.region.as_buffer())
|
|
control = payload.get("control") if isinstance(payload, dict) else None
|
|
if isinstance(control, dict):
|
|
self._snapshot = KernelControlSnapshot(**control)
|
|
|
|
def close(self) -> None:
|
|
self.region.close()
|
|
|
|
def read(self) -> KernelControlSnapshot:
|
|
payload = _decode_packet(self.region.as_buffer())
|
|
control = payload.get("control") if isinstance(payload, dict) else None
|
|
if not isinstance(control, dict):
|
|
return self._snapshot
|
|
self._snapshot = KernelControlSnapshot(**control)
|
|
return self._snapshot
|
|
|
|
def update(self, update: ControlUpdate) -> KernelControlSnapshot:
|
|
self._snapshot = update.apply(self.read())
|
|
self._seq += 1
|
|
self._write_region(self._seq, self._snapshot.as_dict())
|
|
return self._snapshot
|
|
|
|
def mirror(self) -> Dict[str, Any]:
|
|
return self._snapshot.as_dict()
|
|
|
|
def wait(self, timeout_ms: int = 1000) -> bool:
|
|
try:
|
|
return bool(self.region.wait(timeout_ms))
|
|
except Exception:
|
|
return False
|
|
|
|
def notify(self) -> None:
|
|
try:
|
|
self.region.notify()
|
|
except Exception:
|
|
pass
|
|
|
|
def _write_region(self, seq: int, control: Dict[str, Any]) -> None:
|
|
packet = _encode_packet(seq, {"control": control})
|
|
buf = self.region.as_buffer()
|
|
if len(packet) > len(buf):
|
|
raise ValueError(f"payload too large for Zinc control region: {len(packet)} > {len(buf)}")
|
|
view = memoryview(buf)
|
|
view[: len(packet)] = packet
|
|
if len(view) > len(packet):
|
|
view[len(packet) :] = b"\x00" * (len(view) - len(packet))
|
|
try:
|
|
self.region.notify()
|
|
except Exception:
|
|
pass
|