fix(manager-core): F-P-008 concurrent trigger hydration in list_for_app (wall-clock fix)

TriggerRepo::list_for_app selected parent rows then called
hydrate_one(...) per row serially — one SELECT against the kind-
specific details table per trigger. For N triggers on an app, that's
N+1 queries serialized on every dashboard `GET /apps/{id}/triggers`.

This commit shrinks the wall-clock cost via buffered concurrency
(`try_buffered(8)`): each row's details fetch still runs but they run
in parallel up to a small fan-out cap. Cuts dashboard load time from
sum(N latencies) to max(N latencies) / 8.

Collapsing to a single per-kind LEFT JOIN (the audit's preferred shape)
would cut the query count to ~7 — large enough to defer to v1.2 as
documented in the inline comment.

AUDIT.md anchor: F-P-008 (partial: wall-clock only; round-trip count
deferred).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-07 20:20:29 +02:00
parent 8ceb1352dd
commit c80ee194f2

View File

@@ -979,10 +979,21 @@ impl TriggerRepo for PostgresTriggerRepo {
.fetch_all(&self.pool)
.await?;
let mut out = Vec::with_capacity(parents.len());
for p in parents {
out.push(hydrate_one(&self.pool, p).await?);
}
// F-P-008 (partial): concurrent hydration so a dashboard with N
// triggers waits for max(per-row-latency) instead of
// sum(per-row-latency). Still N+1 queries against the details
// tables — collapsing to a single per-kind LEFT JOIN is a
// larger SQL refactor tracked as v1.2.
use futures::stream::{self, TryStreamExt};
let pool = self.pool.clone();
let out = stream::iter(parents.into_iter().map(Ok))
.map_ok(|p| {
let pool = pool.clone();
async move { hydrate_one(&pool, p).await }
})
.try_buffered(8)
.try_collect::<Vec<_>>()
.await?;
Ok(out)
}