Let Postgres fold the rollups, and say when a run list was cut short
/observability/timeseries and /flows read every metric_minute row in the window
and folded them in Python, so the 7d preset pulled a week of rows on each 30 s
poll. date_bin() does the binning now — the row count drops to the slices asked
for, and to flows × 60 for the sparklines. A window of zero hours used to divide
by nothing and answer 500; windows are clamped to an hour at the low end and to
the retention period at the high end, past which there is nothing to find.
/observability/runs returns {data, count} rather than a bare list, so a minute
busier than the 200-row cap says so instead of quietly showing its newest 200.
The count is only queried when the page comes back full, which keeps the poll
from handing back what the fold just saved.
failures_24h leaves the summary — the Home tile counts errors over the selected
window from the rollups, and nothing had read the field since.
Deleting a flow now takes its Run rows and their nodes, metrics and artifacts
with it. This lives in the route rather than in forget_flow because renaming a
flow calls that too, and a rename must keep its history. The observability
rollups stay: they are the record of what ran, and retention already prunes them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uq8mtNb97A7praJLyeEYgs
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import func
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models import Run, RunArtifact, RunMetric, RunNode
|
||||
|
||||
PREFIX = f"{settings.API_V1_STR}/flows"
|
||||
|
||||
@@ -273,9 +278,14 @@ def test_unconnected_input_is_surfaced(
|
||||
|
||||
|
||||
def test_delete_flow(
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
client: TestClient, superuser_token_headers: dict[str, str], db: Session
|
||||
) -> None:
|
||||
client.put(f"{PREFIX}/demo", headers=superuser_token_headers, json=a_flow())
|
||||
db.add(Run(id="run-1", flow="demo", created_at=datetime.now(UTC)))
|
||||
db.add(RunNode(run_id="run-1", node="sensor"))
|
||||
db.add(RunMetric(run_id="run-1", name="loss", step=-1))
|
||||
db.add(RunArtifact(run_id="run-1", name="model.pt"))
|
||||
db.commit()
|
||||
|
||||
assert (
|
||||
client.delete(f"{PREFIX}/demo", headers=superuser_token_headers).status_code
|
||||
@@ -285,6 +295,11 @@ def test_delete_flow(
|
||||
client.get(f"{PREFIX}/demo", headers=superuser_token_headers).status_code == 404
|
||||
)
|
||||
|
||||
# Deleting the flow takes its runs with it, so a reseeded demo starts clean.
|
||||
db.expire_all()
|
||||
for model in (Run, RunNode, RunMetric, RunArtifact):
|
||||
assert db.exec(select(func.count()).select_from(model)).one() == 0
|
||||
|
||||
|
||||
def test_node_types_are_listed(
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
|
||||
Reference in New Issue
Block a user