A wheel whose top-level module is `app` collides with anything else in a user's venv, so the package that is about to be published takes the name it is published under. Only the Python package moves; the repo, the Docker WORKDIR and the compose project keep theirs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
97 lines
3.3 KiB
Python
97 lines
3.3 KiB
Python
import time
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends, Request, Response
|
|
from fastapi.concurrency import run_in_threadpool
|
|
from pydantic.networks import EmailStr
|
|
|
|
from fluksio.api.deps import get_current_active_superuser
|
|
from fluksio.flow.state import RedisState
|
|
from fluksio.models import Message
|
|
from fluksio.utils import generate_test_email, send_email
|
|
|
|
router = APIRouter(prefix="/utils", tags=["utils"])
|
|
|
|
|
|
@router.post(
|
|
"/test-email/",
|
|
dependencies=[Depends(get_current_active_superuser)],
|
|
status_code=201,
|
|
)
|
|
def test_email(email_to: EmailStr) -> Message:
|
|
"""
|
|
Test emails.
|
|
"""
|
|
email_data = generate_test_email(email_to=email_to)
|
|
send_email(
|
|
email_to=email_to,
|
|
subject=email_data.subject,
|
|
html_content=email_data.html_content,
|
|
)
|
|
return Message(message="Test email sent")
|
|
|
|
|
|
@router.get("/health-check/")
|
|
async def health_check() -> bool:
|
|
return True
|
|
|
|
|
|
@router.get("/health/")
|
|
async def health(request: Request, response: Response) -> dict[str, Any]:
|
|
"""Deep health: 200 while the engine can serve its purpose, 503 otherwise.
|
|
|
|
"Serve its purpose" means the event loop is responsive and the configured
|
|
state backend answers — the two failure modes a process-alive check never
|
|
sees. The queue and pool sections are filled by the execution service.
|
|
"""
|
|
controller = getattr(request.app.state, "flow_controller", None)
|
|
watchdog = getattr(request.app.state, "watchdog", None)
|
|
problems: list[str] = []
|
|
|
|
loop_lag = watchdog.snapshot() if watchdog else {"ewma": 0.0, "max_60s": 0.0}
|
|
if watchdog is not None and watchdog.degraded:
|
|
problems.append("event loop lagging")
|
|
|
|
redis_info: dict[str, Any] = {
|
|
"configured": False,
|
|
"connected": None,
|
|
"rtt_ms": None,
|
|
}
|
|
engine: dict[str, Any] = {"flows": 0, "nodes": 0, "quarantined": []}
|
|
queue: dict[str, Any] = {}
|
|
if controller is not None:
|
|
state = controller.state
|
|
if isinstance(state, RedisState):
|
|
redis_info["configured"] = True
|
|
start = time.perf_counter()
|
|
connected = await run_in_threadpool(state.ping)
|
|
redis_info["connected"] = connected
|
|
redis_info["rtt_ms"] = round((time.perf_counter() - start) * 1000, 1)
|
|
if not connected:
|
|
problems.append("redis unreachable")
|
|
# `loaded` is keyed by node id, so its length is a node count.
|
|
loaded = getattr(controller, "loaded", {}) or {}
|
|
engine["flows"] = len({entry.flow for entry in loaded.values()})
|
|
engine["nodes"] = len(
|
|
getattr(getattr(controller, "pipeline", None), "nodes", []) or []
|
|
)
|
|
engine["quarantined"] = sorted(getattr(controller, "quarantined", ()) or ())
|
|
stats = getattr(controller, "queue_stats", None)
|
|
if callable(stats):
|
|
queue = await run_in_threadpool(stats)
|
|
if queue.get("oldest_pending_s", 0) > 120:
|
|
problems.append("queue stalled")
|
|
|
|
status = "degraded" if problems else "ok"
|
|
if problems:
|
|
response.status_code = 503
|
|
return {
|
|
"status": status,
|
|
"problems": problems,
|
|
"loop_lag_ms": loop_lag,
|
|
"redis": redis_info,
|
|
"engine": engine,
|
|
"queue": queue,
|
|
"ts": time.time(),
|
|
}
|