Supervise the engine's host: deep health, loop watchdog, one worker

The API image ran four uvicorn workers, and each one built a full flow
controller — four sets of MQTT subscriptions, cron ticks and webhooks.
Runs one worker now; scaling out is the worker split, not more processes.

Adds a loop-lag watchdog and a deep /utils/health/ that fails when the
event loop is wedged or Redis is unreachable, the two failure modes a
process-alive check never sees. Autoheal restarts on that signal, behind
a compose profile because it mounts the Docker socket.

The private user-seeding routes now need an explicit opt-in rather than
just ENVIRONMENT=local, so a deployment that kept the default never
exposes them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
This commit is contained in:
root
2026-08-16 07:14:28 +02:00
co-authored by Claude Fable 5
parent 8d82d6c4ec
commit 5462842b8a
16 changed files with 325 additions and 13 deletions
+7 -4
View File
@@ -14,6 +14,9 @@ COMPOSE_PROJECT := fluksio-app
# Compose interpolation needs the project-local .env before reading compose.yml. # Compose interpolation needs the project-local .env before reading compose.yml.
COMPOSE := docker compose -p $(COMPOSE_PROJECT) --env-file $(COMPOSE_ROOT)/.env COMPOSE := docker compose -p $(COMPOSE_PROJECT) --env-file $(COMPOSE_ROOT)/.env
COMPOSE_PROD := $(COMPOSE) -f docker/compose.yml COMPOSE_PROD := $(COMPOSE) -f docker/compose.yml
# Production also runs autoheal, which restarts the backend when its deep
# health check fails. It is profile-gated because it mounts the Docker socket.
COMPOSE_PROD_RUN := $(COMPOSE_PROD) --profile autoheal
COMPOSE_DEV := $(COMPOSE_PROD) -f docker/compose.dev.yml COMPOSE_DEV := $(COMPOSE_PROD) -f docker/compose.dev.yml
# Integrated local stack: dev stack wired onto the shared `proxy` network. # Integrated local stack: dev stack wired onto the shared `proxy` network.
COMPOSE_LOCAL := $(COMPOSE_DEV) -f docker/compose.local.yml COMPOSE_LOCAL := $(COMPOSE_DEV) -f docker/compose.local.yml
@@ -34,17 +37,17 @@ dev-local: ## Start the integrated local stack (called by the root `make dev`)
$(COMPOSE_LOCAL) up --build -d proxy db adminer prestart backend frontend mailcatcher $(COMPOSE_LOCAL) up --build -d proxy db adminer prestart backend frontend mailcatcher
up: ## Start the production stack up: ## Start the production stack
$(COMPOSE_PROD) up --build -d $(COMPOSE_PROD_RUN) up --build -d
update: ## Pull, rebuild using the layer cache, and recreate changed containers update: ## Pull, rebuild using the layer cache, and recreate changed containers
git pull git pull
$(COMPOSE_PROD) build $(COMPOSE_PROD_RUN) build
$(COMPOSE_PROD) up -d --remove-orphans $(COMPOSE_PROD_RUN) up -d --remove-orphans
docker image prune -f docker image prune -f
down: ## Stop all running containers down: ## Stop all running containers
-$(COMPOSE_LOCAL) down -$(COMPOSE_LOCAL) down
-$(COMPOSE_PROD) down -$(COMPOSE_PROD_RUN) down
# ── Development (local, no Docker) ─────────────────────────────── # ── Development (local, no Docker) ───────────────────────────────
# Run `make dev-backend` and `make dev-frontend` in two separate terminals. # Run `make dev-backend` and `make dev-frontend` in two separate terminals.
+4
View File
@@ -72,6 +72,10 @@ external interfaces. See `docs/architecture/structure.canvas` → *Backend M
wakes its node, at most every n seconds. State keeps the latest value, so wakes its node, at most every n seconds. State keeps the latest value, so
only the delivery is skipped only the delivery is skipped
- [ ] Alert / notification handler - [ ] Alert / notification handler
- [x] Deep health check (`GET /utils/health/`): reports event-loop lag and state-backend
reachability and fails the container healthcheck, so a wedged engine is restarted
rather than counted as up. One engine per deployment — the API image runs a single
worker, because a second one would be a second engine
- [ ] Test nodes: a small node dragged onto an existing one, smoke or unit, blocking - [ ] Test nodes: a small node dragged onto an existing one, smoke or unit, blocking
deployment on failure deployment on failure
- [ ] User management scoped per flow and per data set - [ ] User management scoped per flow and per data set
+4 -1
View File
@@ -42,4 +42,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
WORKDIR /app/backend/ WORKDIR /app/backend/
CMD ["fastapi", "run", "--workers", "4", "app/main.py"] # Single worker on purpose: the process hosts the flow engine, and a second
# worker would be a second engine — duplicated subscriptions, cron ticks and
# webhooks. Scaling out is the M5 worker split, not more uvicorn processes.
CMD ["fastapi", "run", "app/main.py"]
+3 -3
View File
@@ -1,7 +1,6 @@
from fastapi import APIRouter from fastapi import APIRouter
from app.api.routes import flows, login, oauth, private, secrets, users, utils from app.api.routes import flows, login, oauth, private, secrets, users, utils
from app.core.config import settings
api_router = APIRouter() api_router = APIRouter()
api_router.include_router(login.router) api_router.include_router(login.router)
@@ -15,5 +14,6 @@ api_router.include_router(secrets.router)
api_router.include_router(oauth.router) api_router.include_router(oauth.router)
if settings.ENVIRONMENT == "local": # Always mounted like oauth, so the generated SDK keeps its shape; the
api_router.include_router(private.router) # endpoints refuse to work unless the private API is explicitly enabled.
api_router.include_router(private.router)
+8 -1
View File
@@ -1,9 +1,10 @@
from typing import Any from typing import Any
from fastapi import APIRouter from fastapi import APIRouter, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
from app.api.deps import SessionDep from app.api.deps import SessionDep
from app.core.config import settings
from app.core.security import get_password_hash from app.core.security import get_password_hash
from app.models import ( from app.models import (
User, User,
@@ -13,6 +14,11 @@ from app.models import (
router = APIRouter(tags=["private"], prefix="/private") router = APIRouter(tags=["private"], prefix="/private")
def _require_private_api() -> None:
if not (settings.ENVIRONMENT == "local" and settings.PRIVATE_API_ENABLED):
raise HTTPException(status_code=403, detail="Private API is disabled")
class PrivateUserCreate(BaseModel): class PrivateUserCreate(BaseModel):
email: str email: str
password: str password: str
@@ -25,6 +31,7 @@ def create_user(user_in: PrivateUserCreate, session: SessionDep) -> Any:
""" """
Create a new user. Create a new user.
""" """
_require_private_api()
user = User( user = User(
email=user_in.email, email=user_in.email,
+64 -1
View File
@@ -1,7 +1,12 @@
from fastapi import APIRouter, Depends 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 pydantic.networks import EmailStr
from app.api.deps import get_current_active_superuser from app.api.deps import get_current_active_superuser
from app.flow.state import RedisState
from app.models import Message from app.models import Message
from app.utils import generate_test_email, send_email from app.utils import generate_test_email, send_email
@@ -29,3 +34,61 @@ def test_email(email_to: EmailStr) -> Message:
@router.get("/health-check/") @router.get("/health-check/")
async def health_check() -> bool: async def health_check() -> bool:
return True 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")
engine["flows"] = len(getattr(controller, "loaded", {}) or {})
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(),
}
+4
View File
@@ -44,6 +44,10 @@ class Settings(BaseSettings):
# The MCP endpoint, and the OAuth server agents authenticate against. Off # The MCP endpoint, and the OAuth server agents authenticate against. Off
# until someone asks for it: it opens client registration to the network. # until someone asks for it: it opens client registration to the network.
MCP_ENABLED: bool = False MCP_ENABLED: bool = False
# Unauthenticated test-only endpoints (user seeding). Requires an explicit
# opt-in on top of ENVIRONMENT=local, so a deployment that merely kept the
# default environment never exposes them.
PRIVATE_API_ENABLED: bool = False
DOMAIN: str = "localhost" DOMAIN: str = "localhost"
OAUTH_PRIVATE_KEY_FILE: Path = Path("flow-data/oauth-key.pem") OAUTH_PRIVATE_KEY_FILE: Path = Path("flow-data/oauth-key.pem")
OAUTH_CODE_EXPIRE_SECONDS: int = 60 OAUTH_CODE_EXPIRE_SECONDS: int = 60
+83
View File
@@ -0,0 +1,83 @@
"""Engine self-observation: event-loop lag watchdog and the deliberate exit.
Docker's restart policy only fires when the process exits, so a wedged event
loop would otherwise stay "up" forever. The watchdog measures how late a
sleeping task wakes up — the standard loop-lag trick — and feeds the deep
health endpoint; `engine_fatal` is the deliberate handoff to the outer
supervisor when a clean restart beats limping on.
"""
from __future__ import annotations
import asyncio
import logging
import os
import time
from collections import deque
from app.flow.events import EventBus
logger = logging.getLogger(__name__)
INTERVAL = 1.0
EWMA_ALPHA = 0.2
# One late wake-up is a busy moment; several in a row is a blocked loop.
DEGRADED_LAG_S = 5.0
DEGRADED_STRIKES = 3
# Sustained lag above this marks the engine degraded in /health.
DEGRADED_EWMA_MS = 200.0
class LoopWatchdog:
"""Measures event-loop lag and reports it as engine health."""
def __init__(self, events: EventBus | None = None) -> None:
self._events = events
self.ewma_ms = 0.0
self._window: deque[tuple[float, float]] = deque() # (monotonic ts, lag ms)
self._strikes = 0
async def run(self) -> None:
while True:
before = time.monotonic()
await asyncio.sleep(INTERVAL)
self._record(max(0.0, time.monotonic() - before - INTERVAL))
def _record(self, lag_s: float) -> None:
lag_ms = lag_s * 1000.0
self.ewma_ms += EWMA_ALPHA * (lag_ms - self.ewma_ms)
now = time.monotonic()
self._window.append((now, lag_ms))
while self._window and self._window[0][0] < now - 60.0:
self._window.popleft()
if lag_s >= DEGRADED_LAG_S:
self._strikes += 1
if self._strikes == DEGRADED_STRIKES and self._events is not None:
logger.warning("event loop lagging: %.1fs late", lag_s)
self._events.publish(
{
"type": "engine_degraded",
"reason": f"event loop lag {lag_s:.1f}s",
"ts": time.time(),
}
)
else:
self._strikes = 0
@property
def degraded(self) -> bool:
return self.ewma_ms > DEGRADED_EWMA_MS
def snapshot(self) -> dict[str, float]:
return {
"ewma": round(self.ewma_ms, 1),
"max_60s": round(max((lag for _, lag in self._window), default=0.0), 1),
}
def engine_fatal(reason: str, events: EventBus | None = None) -> None:
"""Log, tell whoever still listens, and exit so Docker restarts us clean."""
logger.critical("engine fatal: %s", reason)
if events is not None:
events.publish({"type": "engine_fatal", "reason": reason, "ts": time.time()})
os._exit(1)
+5
View File
@@ -19,6 +19,7 @@ from app.flow.plugins import load_plugins
from app.flow.secrets import init_secrets from app.flow.secrets import init_secrets
from app.flow.state import MemoryState, RedisState, StateBackend from app.flow.state import MemoryState, RedisState, StateBackend
from app.flow.store import FlowStore from app.flow.store import FlowStore
from app.flow.watchdog import LoopWatchdog
def custom_generate_unique_id(route: APIRoute) -> str: def custom_generate_unique_id(route: APIRoute) -> str:
@@ -62,6 +63,9 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
fastapi_app=app, fastapi_app=app,
) )
app.state.flow_controller = controller app.state.flow_controller = controller
watchdog = LoopWatchdog(event_bus)
app.state.watchdog = watchdog
watchdog_task = asyncio.create_task(watchdog.run(), name="loop-watchdog")
await controller.start() await controller.start()
try: try:
# A mounted sub-app gets no lifespan of its own, so the MCP session # A mounted sub-app gets no lifespan of its own, so the MCP session
@@ -69,6 +73,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
async with _mcp_sessions(): async with _mcp_sessions():
yield yield
finally: finally:
watchdog_task.cancel()
await controller.stop() await controller.stop()
if settings.MCP_ENABLED: if settings.MCP_ENABLED:
from app.mcp.http import aclose from app.mcp.http import aclose
+3
View File
@@ -13,3 +13,6 @@ os.environ["POSTGRES_DB"] = "app_test"
# builds a TestClient — and so a lifespan — per test module. Tests that want the # builds a TestClient — and so a lifespan — per test module. Tests that want the
# endpoint mount it themselves. # endpoint mount it themselves.
os.environ["MCP_ENABLED"] = "false" os.environ["MCP_ENABLED"] = "false"
# The private seeding endpoints are opt-in; the suite is one of the two places
# (with the dev stack) where they are meant to work.
os.environ["PRIVATE_API_ENABLED"] = "true"
+20
View File
@@ -1,3 +1,4 @@
import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlmodel import Session, select from sqlmodel import Session, select
@@ -24,3 +25,22 @@ def test_create_user(client: TestClient, db: Session) -> None:
assert user assert user
assert user.email == "pollo@listo.com" assert user.email == "pollo@listo.com"
assert user.full_name == "Pollo Listo" assert user.full_name == "Pollo Listo"
def test_creating_a_user_is_refused_unless_the_private_api_is_enabled(
client: TestClient, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The route is always mounted so the SDK keeps its shape; the opt-in is
what decides whether unauthenticated user seeding actually works."""
monkeypatch.setattr(settings, "PRIVATE_API_ENABLED", False)
r = client.post(
f"{settings.API_V1_STR}/private/users/",
json={
"email": "nobody@listo.com",
"password": "password123",
"full_name": "Nobody",
},
)
assert r.status_code == 403
+68
View File
@@ -0,0 +1,68 @@
"""Loop-lag watchdog: what the deep health check reads."""
from fastapi.testclient import TestClient
from app.flow.events import EventBus
from app.flow.watchdog import DEGRADED_STRIKES, LoopWatchdog
from app.main import app
def test_a_responsive_loop_stays_healthy():
watchdog = LoopWatchdog()
for _ in range(10):
watchdog._record(0.002)
assert not watchdog.degraded
assert watchdog.snapshot()["max_60s"] == 2.0
def test_sustained_lag_marks_the_engine_degraded():
watchdog = LoopWatchdog()
for _ in range(20):
watchdog._record(1.0)
assert watchdog.degraded
def test_a_blocked_loop_is_announced_once_it_keeps_happening():
events = []
bus = EventBus()
bus.publish = events.append # type: ignore[method-assign]
watchdog = LoopWatchdog(bus)
for _ in range(DEGRADED_STRIKES - 1):
watchdog._record(6.0)
assert events == []
watchdog._record(6.0)
assert [e["type"] for e in events] == ["engine_degraded"]
# A recovered loop resets the count, so the next stall is announced again.
watchdog._record(0.001)
for _ in range(DEGRADED_STRIKES):
watchdog._record(6.0)
assert len(events) == 2
def test_health_reports_503_once_the_loop_is_wedged():
"""The point of the deep check: unhealthy without the process being dead."""
watchdog = LoopWatchdog()
app.state.watchdog = watchdog
# No lifespan here, so there is no controller either — the endpoint has to
# cope with a half-built app rather than assume the engine is up.
client = TestClient(app)
try:
assert client.get("/api/v1/utils/health/").status_code == 200
for _ in range(20):
watchdog._record(1.0)
response = client.get("/api/v1/utils/health/")
assert response.status_code == 503
body = response.json()
assert body["status"] == "degraded"
assert body["problems"] == ["event loop lagging"]
finally:
del app.state.watchdog
+2
View File
@@ -77,6 +77,8 @@ services:
SMTP_PORT: "1025" SMTP_PORT: "1025"
SMTP_TLS: "false" SMTP_TLS: "false"
EMAILS_FROM_EMAIL: "noreply@fluksio.com" EMAILS_FROM_EMAIL: "noreply@fluksio.com"
# Test-only user seeding, needed by the Playwright suite. Dev stack only.
PRIVATE_API_ENABLED: "true"
mailcatcher: mailcatcher:
image: schickling/mailcatcher image: schickling/mailcatcher
+27 -1
View File
@@ -154,16 +154,20 @@ services:
volumes: volumes:
- app-flow-data:/data - app-flow-data:/data
# Deep health: fails when the event loop is wedged or Redis is gone, not
# just when the process is dead. Autoheal restarts on unhealthy.
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/utils/health-check/"] test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/utils/health/"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
start_period: 30s
build: build:
context: .. context: ..
dockerfile: backend/Dockerfile dockerfile: backend/Dockerfile
labels: labels:
- autoheal=true
- traefik.enable=true - traefik.enable=true
- traefik.docker.network=proxy - traefik.docker.network=proxy
- traefik.constraint-label=proxy - traefik.constraint-label=proxy
@@ -212,6 +216,28 @@ services:
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.middlewares=https-redirect - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.middlewares=https-redirect
# Docker never restarts a merely *unhealthy* container on its own; autoheal
# closes that gap for the services labeled autoheal=true.
#
# Behind a profile because it needs the Docker socket, which is host-wide
# authority: on a machine that runs anything besides this stack, that is a
# deliberate operator decision. `make up` opts in; the dev stacks do not.
autoheal:
image: willfarrell/autoheal:latest
container_name: fluksio-autoheal
profiles: ["autoheal"]
restart: always
security_opt:
- no-new-privileges:true
networks:
- default
environment:
# Scoped by label, so it only ever restarts this stack's backend.
- AUTOHEAL_CONTAINER_LABEL=autoheal
- AUTOHEAL_INTERVAL=15
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
volumes: volumes:
app-db-data: app-db-data:
app-redis-data: app-redis-data:
+18 -1
View File
@@ -3,7 +3,7 @@
import type { CancelablePromise } from './core/CancelablePromise'; import type { CancelablePromise } from './core/CancelablePromise';
import { OpenAPI } from './core/OpenAPI'; import { OpenAPI } from './core/OpenAPI';
import { request as __request } from './core/request'; import { request as __request } from './core/request';
import type { FlowsReadFlowsResponse, FlowsReadNodeTypesResponse, FlowsReadLibraryResponse, FlowsDeleteSharedNodeData, FlowsDeleteSharedNodeResponse, FlowsReadFlowData, FlowsReadFlowResponse, FlowsSaveFlowData, FlowsSaveFlowResponse, FlowsDeleteFlowData, FlowsDeleteFlowResponse, FlowsPublishFlowData, FlowsPublishFlowResponse, FlowsDiscardDraftData, FlowsDiscardDraftResponse, FlowsRenameFlowData, FlowsRenameFlowResponse, FlowsReadNodeSourceData, FlowsReadNodeSourceResponse, FlowsSaveNodeSourceData, FlowsSaveNodeSourceResponse, FlowsShareNodeData, FlowsShareNodeResponse, FlowsUnshareNodeData, FlowsUnshareNodeResponse, FlowsStartFlowData, FlowsStartFlowResponse, FlowsStopFlowData, FlowsStopFlowResponse, FlowsPauseFlowData, FlowsPauseFlowResponse, FlowsResumeFlowData, FlowsResumeFlowResponse, FlowsValidateFlowData, FlowsValidateFlowResponse, FlowsRunFlowData, FlowsRunFlowResponse, FlowsTriggerNodeData, FlowsTriggerNodeResponse, FlowsReadFlowStateData, FlowsReadFlowStateResponse, FlowsReadMessageHistoryData, FlowsReadMessageHistoryResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, OauthRegisterClientData, OauthRegisterClientResponse, OauthAuthorizeValidateData, OauthAuthorizeValidateResponse, OauthAuthorizeData, OauthAuthorizeResponse, OauthTokenData, OauthTokenResponse, PrivateCreateUserData, PrivateCreateUserResponse, SecretsReadSecretsResponse, SecretsSaveSecretData, SecretsSaveSecretResponse, SecretsDeleteSecretData, SecretsDeleteSecretResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse } from './types.gen'; import type { FlowsReadFlowsResponse, FlowsReadNodeTypesResponse, FlowsReadLibraryResponse, FlowsDeleteSharedNodeData, FlowsDeleteSharedNodeResponse, FlowsReadFlowData, FlowsReadFlowResponse, FlowsSaveFlowData, FlowsSaveFlowResponse, FlowsDeleteFlowData, FlowsDeleteFlowResponse, FlowsPublishFlowData, FlowsPublishFlowResponse, FlowsDiscardDraftData, FlowsDiscardDraftResponse, FlowsRenameFlowData, FlowsRenameFlowResponse, FlowsReadNodeSourceData, FlowsReadNodeSourceResponse, FlowsSaveNodeSourceData, FlowsSaveNodeSourceResponse, FlowsShareNodeData, FlowsShareNodeResponse, FlowsUnshareNodeData, FlowsUnshareNodeResponse, FlowsStartFlowData, FlowsStartFlowResponse, FlowsStopFlowData, FlowsStopFlowResponse, FlowsPauseFlowData, FlowsPauseFlowResponse, FlowsResumeFlowData, FlowsResumeFlowResponse, FlowsValidateFlowData, FlowsValidateFlowResponse, FlowsRunFlowData, FlowsRunFlowResponse, FlowsTriggerNodeData, FlowsTriggerNodeResponse, FlowsReadFlowStateData, FlowsReadFlowStateResponse, FlowsReadMessageHistoryData, FlowsReadMessageHistoryResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, OauthRegisterClientData, OauthRegisterClientResponse, OauthAuthorizeValidateData, OauthAuthorizeValidateResponse, OauthAuthorizeData, OauthAuthorizeResponse, OauthTokenData, OauthTokenResponse, PrivateCreateUserData, PrivateCreateUserResponse, SecretsReadSecretsResponse, SecretsSaveSecretData, SecretsSaveSecretResponse, SecretsDeleteSecretData, SecretsDeleteSecretResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse, UtilsHealthResponse } from './types.gen';
export class FlowsService { export class FlowsService {
/** /**
@@ -1015,4 +1015,21 @@ export class UtilsService {
url: '/api/v1/utils/health-check/' url: '/api/v1/utils/health-check/'
}); });
} }
/**
* Health
* 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.
* @returns unknown Successful Response
* @throws ApiError
*/
public static health(): CancelablePromise<UtilsHealthResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/utils/health/'
});
}
} }
+4
View File
@@ -668,3 +668,7 @@ export type UtilsTestEmailData = {
export type UtilsTestEmailResponse = (Message); export type UtilsTestEmailResponse = (Message);
export type UtilsHealthCheckResponse = (boolean); export type UtilsHealthCheckResponse = (boolean);
export type UtilsHealthResponse = ({
[key: string]: unknown;
});