chore(consistency): sync task status from DB [auto]
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
This commit is contained in:
parent
7641fcde40
commit
f01b765668
20 changed files with 1027 additions and 165 deletions
141
rein_aharness/execution_cancel.py
Normal file
141
rein_aharness/execution_cancel.py
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue