fix(traefik): the rows page reads every router's history in one index-only query (#5355)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m18s
CI / unit (push) Successful in 1m28s
CI / publish (push) Successful in 2m3s

#5355 "Traefik page doesn't load": /plugins/traefik/rows took 12-28 s.
It ran a query per router ever seen, and each raw row in the range was a
heap fetch. traefik_004 rebuilds the router index to cover the charted
columns and indexes scraped_at for the request chart; router_history reads
the current routers' buckets in one query.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 08:20:09 -04:00
co-authored by Claude Opus 5.5
parent 96c1ea9407
commit 31e93e96cf
3 changed files with 109 additions and 38 deletions
+34
View File
@@ -74,3 +74,37 @@ def test_the_request_chart_sums_routers_and_weights_the_5xx_share(app):
got = asyncio.run(go())
# One bucket: totals 4 and 6 average to 5; 150 weighted 5xx over 10 req/s is 15%.
assert got == [{"at": at, "req_rate": 5.0, "err_pct": 15.0}]
def test_router_history_reads_every_router_in_one_query_bucketed(app):
"""#5214: the rows page read each router's history in its own query, a
heap fetch per row, and took 12-28 s. One query now buckets every named
router; a router not asked for is not read, and one with no rows in the
range still has an (empty) entry."""
from plugins.traefik.models import TraefikMetric
from plugins.traefik.routes import router_history
at = datetime(2098, 6, 1, 12, 0, tzinfo=timezone.utc) + timedelta(minutes=5 * (uuid.uuid4().int % 2_000))
tag = uuid.uuid4().hex[:8]
a, b, c, idle = (f"{x}-{tag}@docker" for x in ("a", "b", "c", "idle"))
def row(name, minute, rate, p95=2.0):
return TraefikMetric(router_name=name, scraped_at=at + timedelta(minutes=minute), request_rate=rate,
error_rate_4xx_pct=0.0, error_rate_5xx_pct=0.0, latency_p50_ms=1.0,
latency_p95_ms=p95, latency_p99_ms=3.0, response_bytes_rate=0.0)
async def go():
async with app.db_sessionmaker() as s:
async with s.begin():
s.add_all([row(a, 0, 1.0, 10.0), row(a, 1, 3.0, 30.0), row(a, 5, 7.0),
row(b, 0, 4.0), row(c, 0, 9.0), row(idle, -60, 5.0)])
async with s.begin():
return await router_history(s, [a, b, idle], at, 300)
got = asyncio.run(go())
assert set(got) == {a, b, idle} # c was not asked for
# a: minutes 0 and 1 share the first 5-minute bucket and average; minute 5 starts the next.
assert [r.request_rate for r in got[a]] == [2.0, 7.0]
assert got[a][0].latency_p95_ms == 20.0
assert [r.request_rate for r in got[b]] == [4.0]
assert got[idle] == [] # its only row is before the range