From c80ee194f2140b4c4a61dbb1c202da20b2967201 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sun, 7 Jun 2026 20:20:29 +0200 Subject: [PATCH] fix(manager-core): F-P-008 concurrent trigger hydration in list_for_app (wall-clock fix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- crates/manager-core/src/trigger_repo.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/crates/manager-core/src/trigger_repo.rs b/crates/manager-core/src/trigger_repo.rs index 9d8c22a..8a725de 100644 --- a/crates/manager-core/src/trigger_repo.rs +++ b/crates/manager-core/src/trigger_repo.rs @@ -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::>() + .await?; Ok(out) }