precedentai / shared /execution.py
lothnic's picture
Enable private Supabase accounts, saved history and durable budgets
c731c95 verified
Raw History Blame Contribute Delete
3.28 kB
"""Cooperative cancellation and deadlines for one process's active graph work."""
from __future__ import annotations
import threading
import time
from contextvars import ContextVar
from functools import wraps
from uuid import UUID
class ExecutionStopped(RuntimeError):
def __init__(self, reason: str):
self.reason = reason
self.code = "research_timeout" if reason == "deadline" else "research_cancelled"
super().__init__(
"Research took too long. Try a more focused question."
if reason == "deadline"
else "Research was stopped."
)
class Execution:
def __init__(self, seconds: float):
self.deadline = time.monotonic() + seconds
self.reason = ""
self.stopped = threading.Event()
self.idle = threading.Event()
self.idle.set()
self._lock = threading.RLock()
self._workers = 0
def stop(self, reason: str = "cancelled") -> None:
with self._lock:
if not self.reason:
self.reason = reason
self.stopped.set()
def check(self) -> None:
if time.monotonic() >= self.deadline:
self.stop("deadline")
if self.stopped.is_set():
raise ExecutionStopped(self.reason)
def enter(self) -> None:
with self._lock:
self.check()
self._workers += 1
self.idle.clear()
def leave(self) -> None:
with self._lock:
self._workers -= 1
if not self._workers:
self.idle.set()
execution: ContextVar[Execution | None] = ContextVar("precedent_execution", default=None)
_active: dict[tuple[str, UUID], Execution] = {}
_registry_lock = threading.Lock()
def register(owner: str, ident: UUID, control: Execution) -> bool:
with _registry_lock:
key = (owner, ident)
if key in _active:
return False
_active[key] = control
return True
def unregister(owner: str, ident: UUID, control: Execution) -> None:
with _registry_lock:
if _active.get((owner, ident)) is control:
del _active[(owner, ident)]
def stop_request(owner: str, ident: UUID) -> bool:
with _registry_lock:
control = _active.get((owner, ident))
if control is None:
return False
control.stop()
return True
def check_execution() -> None:
control = execution.get()
if control is not None:
control.check()
def remaining_seconds(maximum: float) -> float:
check_execution()
control = execution.get()
return min(maximum, max(0.01, control.deadline - time.monotonic())) if control else maximum
def pause(seconds: float) -> None:
control = execution.get()
if control:
control.stopped.wait(remaining_seconds(seconds))
control.check()
else:
time.sleep(seconds)
def guarded_node(node):
@wraps(node)
def guarded(*args, **kwargs):
control = execution.get()
if control is None:
return node(*args, **kwargs)
control.enter()
try:
result = node(*args, **kwargs)
control.check()
return result
finally:
control.leave()
return guarded