Backend: real alert test results, state cleanup on delete/rename, node trigger errors, queue and collector fixes
`AlertManager.send` swallowed every delivery failure, so the alerts screen's Test button answered 200 whatever happened — the one thing it exists for. It takes `raise_on_error` now, which only the test route passes; the per-channel loop keeps the swallow, because one dead channel must not stop the others hearing about the same fault. A refused delivery answers 502 with whatever the sender said. Renaming a flow left its values under the old name for good: the delete path already swept them, the rename path never did. It calls the same `forget_flow`, which covers the messages and the `__ts__`/`__version__`/`__history__` bookkeeping keyed by message name. Cleanup, not migration — they repopulate under the new name on the next run. Triggering a node by hand ran `Node.__call__` with nothing catching it, so a node that raised produced a 500 and a stack trace in the server log, and nothing at all on the canvas. `Pipeline.publish_error` is the reporting half of `_execute_node` lifted out; both paths go through it, so a manual failure now reads the same on the canvas and in the metrics as a queued one. The route answers 400 with the node's error. `MemoryWorkQueue.stats()` counts claimed-but-unacknowledged work rather than reporting zero, so the health tile means something without Redis. The metrics collector's held tracebacks are capped at `DETAIL_CAP` and swept on the same `RUN_STALE_S` cutoff the open runs use, instead of one untruncated traceback per node kept for the life of the process — a traceback still survives the flush between the log and the failure it belongs to. `GET /observability/events` takes `since`/`until`, the window `/runs` already took, so a failures list can cover the span the charts beside it are drawn from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
This commit is contained in:
@@ -74,12 +74,18 @@ async def test_channel(channel_name: str, controller: FlowControllerDep) -> Any:
|
||||
if channel is None:
|
||||
raise HTTPException(status_code=404, detail=f"No channel '{channel_name}'")
|
||||
|
||||
await controller.alerts.send(
|
||||
channel,
|
||||
Alert(
|
||||
title="Fluksio test alert",
|
||||
body="If you are reading this, the channel works.",
|
||||
severity="warning",
|
||||
),
|
||||
)
|
||||
try:
|
||||
await controller.alerts.send(
|
||||
channel,
|
||||
Alert(
|
||||
title="Fluksio test alert",
|
||||
body="If you are reading this, the channel works.",
|
||||
severity="warning",
|
||||
),
|
||||
raise_on_error=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
# Whatever the sender said, verbatim: it is the only clue the operator
|
||||
# has about why the channel does not work.
|
||||
raise HTTPException(status_code=502, detail=str(exc) or type(exc).__name__)
|
||||
return Message(message=f"Sent a test alert through '{channel_name}'")
|
||||
|
||||
@@ -432,6 +432,10 @@ async def rename_flow(
|
||||
except FlowExists as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc))
|
||||
|
||||
# The old name is nobody's namespace now; its values would sit there under a
|
||||
# flow that no longer exists. They repopulate under the new name on the next
|
||||
# run, so this is cleanup rather than a migration.
|
||||
await run_in_threadpool(controller.forget_flow, name)
|
||||
await controller.reload()
|
||||
return _detail(controller, renamed)
|
||||
|
||||
@@ -633,13 +637,17 @@ async def trigger_node(
|
||||
"""Feed values into a single node."""
|
||||
_require_enabled(controller, name)
|
||||
try:
|
||||
await run_in_threadpool(
|
||||
error = await run_in_threadpool(
|
||||
controller.trigger_node, f"{name}.{node_id}", body.values
|
||||
)
|
||||
except KeyError:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"No node named '{node_id}' in flow '{name}'"
|
||||
)
|
||||
if error:
|
||||
# The canvas already has the failure from the bus; the person who
|
||||
# clicked gets to read it too, instead of a stack trace in the log.
|
||||
raise HTTPException(status_code=400, detail=error)
|
||||
return _flow_state(controller, name)
|
||||
|
||||
|
||||
|
||||
@@ -306,9 +306,15 @@ def read_events(
|
||||
session: SessionDep,
|
||||
kind: Literal["failure", "audit"] = "failure",
|
||||
flow: str | None = None,
|
||||
since: datetime | None = None,
|
||||
until: datetime | None = None,
|
||||
limit: int = 100,
|
||||
) -> Any:
|
||||
"""What went wrong, or who changed what. Newest first."""
|
||||
"""What went wrong, or who changed what. Newest first.
|
||||
|
||||
``since`` is inclusive and ``until`` exclusive, the same window ``/runs``
|
||||
takes, so a list can cover the span the charts beside it are drawn from.
|
||||
"""
|
||||
statement = select(EngineEvent).order_by(col(EngineEvent.ts).desc())
|
||||
if kind == "audit":
|
||||
statement = statement.where(col(EngineEvent.type) == "audit")
|
||||
@@ -316,6 +322,10 @@ def read_events(
|
||||
statement = statement.where(col(EngineEvent.type) != "audit")
|
||||
if flow:
|
||||
statement = statement.where(col(EngineEvent.flow) == flow)
|
||||
if since:
|
||||
statement = statement.where(col(EngineEvent.ts) >= _aware(since))
|
||||
if until:
|
||||
statement = statement.where(col(EngineEvent.ts) < _aware(until))
|
||||
return list(session.exec(statement.limit(min(limit, 500))))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user