decomposer: generate deliverable files for Define the service contract and project architecture for the FastAPI endpoint monitoring service.; Implement the typed monitor CRUD API and concurrency-safe in-memory state according to the service design.; Implement secure on-demand endpoint checks with status updates, latency measurement, robust error handling, and redacted structured logs.; Add operational API endpoints and environment-driven runtime configuration to the monitoring service.; Create automated tests for the monitoring service.; Package the service with Docker and developer documentation.; Validate the complete project.
Some checks failed
ci / validate (push) Has been cancelled

This commit is contained in:
2026-08-09 15:41:11 +00:00
parent 2ec7897acd
commit b80e213484

View File

@@ -1,68 +1,81 @@
import asyncio import asyncio
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from app.models import CurrentStatus, Monitor, MonitorCreate, MonitorUpdate, utcnow from app.models import CurrentStatus, Monitor, MonitorInput, State, utcnow
class NotFoundError(Exception):
pass
class CapacityError(Exception):
pass
class StaleCheckError(Exception):
pass
class MonitorStore: class MonitorStore:
"""Lock-protected process-local storage returning defensive copies.""" def __init__(self, capacity: int) -> None:
def __init__(self) -> None:
self._lock = asyncio.Lock()
self._items: dict[UUID, Monitor] = {} self._items: dict[UUID, Monitor] = {}
self._revisions: dict[UUID, int] = {} self._lock = asyncio.Lock()
self._capacity = capacity
async def create(self, data: MonitorCreate) -> Monitor: async def create(self, data: MonitorInput) -> Monitor:
now = utcnow()
item = Monitor(id=uuid4(), name=data.name, url=data.url, created_at=now,
updated_at=now, current_status=CurrentStatus())
async with self._lock: async with self._lock:
if len(self._items) >= self._capacity:
raise CapacityError
now = utcnow()
item = Monitor(
id=uuid4(), name=data.name, url=data.url, created_at=now,
updated_at=now, current_status=CurrentStatus(),
)
self._items[item.id] = item self._items[item.id] = item
self._revisions[item.id] = 0
return item.model_copy(deep=True) return item.model_copy(deep=True)
async def list(self) -> list[Monitor]: async def list(self) -> list[Monitor]:
async with self._lock: async with self._lock:
values = sorted(self._items.values(), key=lambda item: item.created_at) return [item.model_copy(deep=True) for item in self._items.values()]
return [item.model_copy(deep=True) for item in values]
async def get(self, monitor_id: UUID) -> Monitor | None: async def get(self, monitor_id: UUID) -> Monitor:
async with self._lock:
item = self._items.get(monitor_id)
return item.model_copy(deep=True) if item else None
async def snapshot(self, monitor_id: UUID) -> tuple[Monitor, int] | None:
async with self._lock: async with self._lock:
item = self._items.get(monitor_id) item = self._items.get(monitor_id)
if item is None: if item is None:
return None raise NotFoundError
return item.model_copy(deep=True), self._revisions[monitor_id] return item.model_copy(deep=True)
async def update(self, monitor_id: UUID, data: MonitorUpdate) -> Monitor | None: async def replace(self, monitor_id: UUID, data: MonitorInput) -> Monitor:
async with self._lock: async with self._lock:
item = self._items.get(monitor_id) current = self._items.get(monitor_id)
if item is None: if current is None:
return None raise NotFoundError
changes = data.model_dump(exclude_none=True) status = current.current_status
changes["updated_at"] = utcnow() if str(current.url) != str(data.url):
updated = item.model_copy(update=changes, deep=True) status = CurrentStatus(state=State.UNKNOWN)
self._items[monitor_id] = updated item = current.model_copy(update={
self._revisions[monitor_id] += 1 "name": data.name, "url": data.url, "updated_at": utcnow(),
return updated.model_copy(deep=True) "current_status": status,
})
self._items[monitor_id] = item
return item.model_copy(deep=True)
async def delete(self, monitor_id: UUID) -> bool: async def delete(self, monitor_id: UUID) -> None:
async with self._lock: async with self._lock:
existed = self._items.pop(monitor_id, None) is not None if self._items.pop(monitor_id, None) is None:
self._revisions.pop(monitor_id, None) raise NotFoundError
return existed
async def record_status(self, monitor_id: UUID, revision: int, async def publish_status(
status: CurrentStatus) -> Monitor | None: self, monitor_id: UUID, expected_url: str, status: CurrentStatus
) -> Monitor:
async with self._lock: async with self._lock:
item = self._items.get(monitor_id) current = self._items.get(monitor_id)
if item is None or self._revisions[monitor_id] != revision: if current is None:
return None raise NotFoundError
updated = item.model_copy(update={"current_status": status, if str(current.url) != expected_url:
"updated_at": utcnow()}, deep=True) raise StaleCheckError
self._items[monitor_id] = updated item = current.model_copy(update={
return updated.model_copy(deep=True) "current_status": status, "updated_at": utcnow()
})
self._items[monitor_id] = item
return item.model_copy(deep=True)