decomposer: generate files for Maintain only the latest check result for each monitored target in concurrency-safe memory and expose it through the specified FastAPI current-status endpoint.
This commit is contained in:
94
src/current_status/coordinator.py
Normal file
94
src/current_status/coordinator.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import copy
|
||||||
|
from collections.abc import Awaitable, Callable, Mapping
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
|
|
||||||
|
class TargetNotFound(KeyError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _TargetState:
|
||||||
|
incarnation: UUID
|
||||||
|
latest_started: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class _Ticket:
|
||||||
|
target_id: str
|
||||||
|
incarnation: UUID
|
||||||
|
sequence: int
|
||||||
|
|
||||||
|
|
||||||
|
class CurrentStatusCoordinator:
|
||||||
|
"""Owns target incarnations and at most one completed status per target."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._lock = asyncio.Lock()
|
||||||
|
self._targets: dict[str, _TargetState] = {}
|
||||||
|
self._statuses: dict[str, dict[str, Any]] = {}
|
||||||
|
|
||||||
|
async def register_target(self, target_id: str) -> None:
|
||||||
|
"""Hook for the prior target-create path."""
|
||||||
|
async with self._lock:
|
||||||
|
if target_id in self._targets:
|
||||||
|
raise ValueError(f"target already exists: {target_id}")
|
||||||
|
self._targets[target_id] = _TargetState(incarnation=uuid4())
|
||||||
|
self._statuses.pop(target_id, None)
|
||||||
|
|
||||||
|
async def delete_target(self, target_id: str) -> bool:
|
||||||
|
"""Hook for target deletion; status removal is in the same critical section."""
|
||||||
|
async with self._lock:
|
||||||
|
existed = self._targets.pop(target_id, None) is not None
|
||||||
|
self._statuses.pop(target_id, None)
|
||||||
|
return existed
|
||||||
|
|
||||||
|
async def _begin_check(self, target_id: str) -> _Ticket:
|
||||||
|
async with self._lock:
|
||||||
|
target = self._targets.get(target_id)
|
||||||
|
if target is None:
|
||||||
|
raise TargetNotFound(target_id)
|
||||||
|
target.latest_started += 1
|
||||||
|
return _Ticket(target_id, target.incarnation, target.latest_started)
|
||||||
|
|
||||||
|
async def _publish(self, ticket: _Ticket, result: Mapping[str, Any]) -> bool:
|
||||||
|
async with self._lock:
|
||||||
|
target = self._targets.get(ticket.target_id)
|
||||||
|
if target is None or target.incarnation != ticket.incarnation:
|
||||||
|
return False
|
||||||
|
if target.latest_started != ticket.sequence:
|
||||||
|
return False
|
||||||
|
self._statuses[ticket.target_id] = {
|
||||||
|
"payload": copy.deepcopy(dict(result)),
|
||||||
|
"checked_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"check_sequence": ticket.sequence,
|
||||||
|
}
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def run_check(
|
||||||
|
self,
|
||||||
|
target_id: str,
|
||||||
|
target: Mapping[str, Any],
|
||||||
|
checker: Callable[[Mapping[str, Any]], Awaitable[Mapping[str, Any]]],
|
||||||
|
) -> bool:
|
||||||
|
"""Run the prior checker and publish only if its ticket is still current."""
|
||||||
|
ticket = await self._begin_check(target_id)
|
||||||
|
result = await checker(target)
|
||||||
|
return await self._publish(ticket, result)
|
||||||
|
|
||||||
|
async def read(self, target_id: str) -> tuple[bool, dict[str, Any] | None]:
|
||||||
|
async with self._lock:
|
||||||
|
if target_id not in self._targets:
|
||||||
|
return False, None
|
||||||
|
status = self._statuses.get(target_id)
|
||||||
|
return True, copy.deepcopy(status)
|
||||||
|
|
||||||
|
async def retained_status_count(self) -> int:
|
||||||
|
async with self._lock:
|
||||||
|
return len(self._statuses)
|
||||||
Reference in New Issue
Block a user