rein-aharness/rein_aharness/lease_monitor.py
tegwick 9097d69e1d Expose lease loss to execution boundaries
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02b6f-7db1-7222-918b-e813a6bda38d
2026-08-23 14:16:11 +02:00

77 lines
2.3 KiB
Python

"""Observable lease state for long-running executions.
The monitor is deliberately independent of an adapter. A heartbeat worker can
mark a lease lost, while the executor observes that fact and decides how to
cancel or refuse acceptance at its own boundary.
"""
from __future__ import annotations
import threading
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Callable
@dataclass(frozen=True)
class LeaseLoss:
"""Bounded evidence that the worker no longer owns a lease."""
error_type: str
observed_at: str
class LeaseMonitor:
"""Thread-safe active/lost/stopped state with a one-shot loss callback."""
def __init__(self, on_lost: Callable[[LeaseLoss], None] | None = None) -> None:
self._lock = threading.Lock()
self._lost = threading.Event()
self._stopped = False
self._evidence: LeaseLoss | None = None
self._on_lost = on_lost
@property
def lost(self) -> bool:
return self._lost.is_set()
@property
def stopped(self) -> bool:
with self._lock:
return self._stopped
@property
def evidence(self) -> LeaseLoss | None:
with self._lock:
return self._evidence
def wait_lost(self, timeout: float | None = None) -> bool:
"""Wait until lease loss is observed; return whether it occurred."""
return self._lost.wait(timeout)
def mark_lost(self, error_type: str) -> LeaseLoss:
"""Record the first loss and invoke the callback once.
Error text is intentionally not retained: provider responses can carry
credentials, prompts, or other unbounded data.
"""
callback: Callable[[LeaseLoss], None] | None = None
with self._lock:
if self._evidence is None:
evidence = LeaseLoss(
error_type=error_type[:80] or "OpsRunError",
observed_at=datetime.now(timezone.utc).isoformat(),
)
self._evidence = evidence
self._lost.set()
callback = self._on_lost
else:
evidence = self._evidence
if callback is not None:
callback(evidence)
return evidence
def stop(self) -> None:
with self._lock:
self._stopped = True