Updated by fix-consistency on 2026-09-04: - update .custodian-brief.md for rein-aharness Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a06ba0-10aa-7ea0-b20a-4f3fac39efe9
141 lines
4.1 KiB
Python
141 lines
4.1 KiB
Python
"""One-shot cancellation for an in-flight adapter call.
|
|
|
|
Lease loss, run timeout, and process signals all need the same adapter-side
|
|
effect: stop the current subprocess or HTTP client without retaining provider
|
|
output. The claim loop owns the reason; adapters only observe and stop.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextvars
|
|
import threading
|
|
from collections.abc import Callable, Iterator
|
|
from contextlib import contextmanager
|
|
from typing import Any
|
|
|
|
_ALLOWED_REASONS = frozenset({"lease-loss", "timeout", "signal"})
|
|
_REASON_LIMIT = 32
|
|
|
|
_active: contextvars.ContextVar["ExecutionCancel | None"] = contextvars.ContextVar(
|
|
"rein_aharness_execution_cancel",
|
|
default=None,
|
|
)
|
|
|
|
|
|
class ExecutionCancelled(RuntimeError):
|
|
"""Bounded proof that an adapter stopped because the run was cancelled."""
|
|
|
|
def __init__(self, reason: str) -> None:
|
|
self.reason = _normalize_reason(reason)
|
|
super().__init__(f"execution cancelled ({self.reason})")
|
|
|
|
|
|
class ExecutionCancel:
|
|
"""Thread-safe one-shot cancel with optional process/HTTP stop callbacks."""
|
|
|
|
def __init__(self) -> None:
|
|
self._lock = threading.Lock()
|
|
self._event = threading.Event()
|
|
self._reason: str | None = None
|
|
self._stops: list[Callable[[], None]] = []
|
|
|
|
@property
|
|
def cancelled(self) -> bool:
|
|
return self._event.is_set()
|
|
|
|
@property
|
|
def reason(self) -> str | None:
|
|
with self._lock:
|
|
return self._reason
|
|
|
|
def wait(self, timeout: float | None = None) -> bool:
|
|
return self._event.wait(timeout)
|
|
|
|
def check(self) -> None:
|
|
if self.cancelled:
|
|
raise ExecutionCancelled(self.reason or "cancelled")
|
|
|
|
def cancel(self, reason: str) -> str:
|
|
"""Record the first reason and invoke registered stop callbacks once."""
|
|
callbacks: list[Callable[[], None]] = []
|
|
with self._lock:
|
|
if self._reason is None:
|
|
self._reason = _normalize_reason(reason)
|
|
self._event.set()
|
|
callbacks = list(self._stops)
|
|
recorded = self._reason
|
|
for callback in callbacks:
|
|
_invoke_stop(callback)
|
|
return recorded
|
|
|
|
def register_stop(self, stop: Callable[[], None]) -> None:
|
|
"""Register a best-effort stopper; invoke immediately if already cancelled."""
|
|
invoke_now = False
|
|
with self._lock:
|
|
self._stops.append(stop)
|
|
invoke_now = self._reason is not None
|
|
if invoke_now:
|
|
_invoke_stop(stop)
|
|
|
|
def register_process(self, proc: Any) -> None:
|
|
"""Kill an in-flight subprocess. Safe against mock and exited processes."""
|
|
self.register_stop(lambda: _kill_process(proc))
|
|
|
|
def evidence(self) -> dict[str, str]:
|
|
if not self.cancelled:
|
|
return {}
|
|
return {"cancelled": "true", "reason": self.reason or "cancelled"}
|
|
|
|
|
|
def active_cancel() -> ExecutionCancel | None:
|
|
return _active.get()
|
|
|
|
|
|
def resolve_cancel(explicit: ExecutionCancel | None = None) -> ExecutionCancel | None:
|
|
return explicit if explicit is not None else active_cancel()
|
|
|
|
|
|
@contextmanager
|
|
def using_cancel(cancel: ExecutionCancel) -> Iterator[ExecutionCancel]:
|
|
token = _active.set(cancel)
|
|
try:
|
|
yield cancel
|
|
finally:
|
|
_active.reset(token)
|
|
|
|
|
|
def _normalize_reason(reason: str) -> str:
|
|
text = (reason or "").strip()[:_REASON_LIMIT]
|
|
if text in _ALLOWED_REASONS:
|
|
return text
|
|
return "cancelled"
|
|
|
|
|
|
def _invoke_stop(stop: Callable[[], None]) -> None:
|
|
try:
|
|
stop()
|
|
except Exception:
|
|
return
|
|
|
|
|
|
def _kill_process(proc: Any) -> None:
|
|
poll = getattr(proc, "poll", None)
|
|
if callable(poll):
|
|
try:
|
|
status = poll()
|
|
except Exception:
|
|
status = None
|
|
if isinstance(status, int):
|
|
return
|
|
kill = getattr(proc, "kill", None)
|
|
if callable(kill):
|
|
try:
|
|
kill()
|
|
except Exception:
|
|
return
|
|
wait = getattr(proc, "wait", None)
|
|
if callable(wait):
|
|
try:
|
|
wait(timeout=2)
|
|
except Exception:
|
|
return
|