Files
hermes-agent/registration_lifecycle.py

96 lines
3.8 KiB
Python

"""Ownership leases for replaceable runtime registrations.
The coordinator models registration *generations*, not just value identity: the same provider
singleton may be registered again after an older ownership generation was unloaded.
"""
from __future__ import annotations
import threading
from collections.abc import Callable, Hashable
from contextlib import contextmanager
from dataclasses import dataclass, field
from typing import Any
def same_registration(left: Any, right: Any) -> bool:
"""Compare opaque registry snapshots using identity only (element-wise for tuples)."""
if isinstance(left, tuple) and isinstance(right, tuple):
return len(left) == len(right) and all(same_registration(a, b) for a, b in zip(left, right))
return left is right
@dataclass
class ReplacementLease:
"""One ownership generation in a replaceable registry slot."""
coordinator: "ReplacementCoordinator"
slot: Hashable
current: Any
previous: Any
restore: Callable[[Any], bool]
finalize: Callable[[], None] | None = None
predecessor: "ReplacementLease | None" = None
active: bool = field(default=True, init=False)
def dispose(self) -> None:
self.coordinator.dispose(self)
class ReplacementCoordinator:
"""Link and remove registration generations in arbitrary unload order."""
def __init__(self) -> None:
self._active: dict[Hashable, list[ReplacementLease]] = {}
self._lock = threading.RLock()
@contextmanager
def transaction(self):
"""Serialize a registry snapshot/write/acquire with lease disposal."""
with self._lock:
yield
def acquire(self, slot: Hashable, *, current: Any, previous: Any, restore: Callable[[Any], bool],
finalize: Callable[[], None] | None = None) -> ReplacementLease:
"""Attach a new live generation to the matching active predecessor."""
with self._lock:
leases = self._active.setdefault(slot, [])
predecessor = next((c for c in reversed(leases) if c.active and same_registration(c.current, previous)), None)
lease = ReplacementLease(self, slot, current, previous, restore, finalize, predecessor)
leases.append(lease)
return lease
def dispose(self, lease: ReplacementLease) -> None:
"""Remove *lease*, restoring the nearest still-live predecessor.
``restore`` -> ``finalize`` -> slot pruning each run even when an earlier step raises.
"""
with self._lock:
if not lease.active:
return
leases = self._active.get(lease.slot, [])
latest = next((c for c in reversed(leases) if c.active), None)
lease.active = False
# An older generation can share the exact object identity of a newer one; registry-level
# CAS cannot tell them apart, so only the latest live generation may mutate the slot.
try:
try:
if latest is lease:
# Restore the nearest still-live predecessor, else the last dead generation's previous.
replacement, predecessor = lease.previous, lease.predecessor
while predecessor is not None and not predecessor.active:
replacement, predecessor = predecessor.previous, predecessor.predecessor
lease.restore(predecessor.current if predecessor is not None else replacement)
finally:
if lease.finalize is not None:
lease.finalize()
finally:
live = [item for item in leases if item.active]
if live:
self._active[lease.slot] = live
elif leases:
self._active.pop(lease.slot, None)
replacement_coordinator = ReplacementCoordinator()