feat: serialize public hardware execution

This commit is contained in:
vee
2026-08-03 09:25:38 +08:00
parent a6d974cb29
commit 85b4740d29
10 changed files with 308 additions and 16 deletions
@@ -5,9 +5,12 @@ from __future__ import annotations
import hashlib
import hmac
import os
import threading
import time
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable
from typing import Any, Callable, Iterator
from cognitive_agent.infrastructure.firmware import (
FIRMWARE_PROTOCOL_SIGNATURE,
@@ -36,6 +39,57 @@ class HardwareSafetyError(ValueError):
"""Raised before any CAN device is opened when a safety gate fails."""
class HardwareBusyError(RuntimeError):
"""Raised when another request already owns the single physical bench."""
def __init__(self, retry_after_seconds: int = 5) -> None:
self.retry_after_seconds = retry_after_seconds
super().__init__(
"真实座椅台架正在执行其他请求。为避免多人指令交叉,本次请求未进入CAN链路;"
"请等待当前动作完成后重试。"
)
class HardwareExecutionCoordinator:
"""Provide process-wide, fail-fast exclusive ownership of the physical bench."""
def __init__(self, retry_after_seconds: int = 5) -> None:
self.retry_after_seconds = retry_after_seconds
self._execution_lock = threading.Lock()
self._state_lock = threading.Lock()
self._active_since: float | None = None
@contextmanager
def reserve(self) -> Iterator[None]:
if not self._execution_lock.acquire(blocking=False):
raise HardwareBusyError(self.retry_after_seconds)
with self._state_lock:
self._active_since = time.monotonic()
try:
yield
finally:
with self._state_lock:
self._active_since = None
self._execution_lock.release()
def public_info(self) -> dict[str, Any]:
with self._state_lock:
active_since = self._active_since
return {
"busy": active_since is not None,
"active_for_ms": (
max(0, int((time.monotonic() - active_since) * 1000))
if active_since is not None
else 0
),
"concurrency_policy": "single_flight_fail_fast",
"retry_after_seconds": self.retry_after_seconds,
}
PHYSICAL_BENCH_COORDINATOR = HardwareExecutionCoordinator()
def _env_bool(name: str, default: bool = False) -> bool:
value = os.getenv(name)
return default if value is None else value.strip().lower() in TRUE_VALUES