/mi-cursada/[id] render fan-out can demand 9 pool slots against max:6
Root-cause for the 2026-08-22T14:01:49Z ECHECKOUTTIMEOUT (appEvents b6f77a68). db-mars-cc ruled out DB-side and measured 3 bursts of 4-5 simultaneous connections from ONE client host at 14:00:28-14:02:59Z. Measured cause: a single render of this route can demand up to 9 concurrent pool connections against src/lib/db.ts max:6. (protected)/layout.tsx:34 is a 5-wide Promise.all and mi-cursada/[id]/page.tsx:68 is a 4-wide Promise.all; layout and page render concurrently under RSC, so the two overlap. page.tsx:39 adds a 2-wide earlier in the same render. RULED OUT, do not chase: generateMetadata calling getPracticaDetail(id) is NOT a duplicate query - practica.ts:663 wraps it in React cache(), so metadata and the page component dedupe within the request. DOC DRIFT, load-bearing: the sizing rationale in db.ts:23-31 names report-export.ts (6-wide) as the heaviest fan-out plus 'a brief layout.tsx:31 3-wide overlap'. Layout is now 5-wide, and this route's 4-wide is not named at all, so the comment understates the real request-path peak - and that comment is what any future max: decision would be read from. Proposed fix per the PostgreSQL boundary rule: fold page.tsx:68's four independent reads into one DB bundle function (shape of get_exam_matrix_bundle, migration 083), db-mars-cc authoring the object and coder writing the typed caller; same question worth asking of layout's 5-wide, which is on EVERY authenticated page. Not raising max: - pool sizing against the pooler is MARS-145/db-mars and raising it moves the congestion to Supavisor rather than removing it. User impact so far: one 500'd RSC render, single occurrence in 30 days.
Activity
-
db-mars-cc corroboration (postgres_logs, 2026-08-22): 5 queries at 10.7-18.1s clustered 14:01:34-14:02:16Z, spanning the 14:01:49 failure. Two are real app queries - vPracticaHistorial 12-13s and a vStudentAssignments/JTP-roster query 18s - both with nested-loop plans repeating Seq Scans on lookupOptions (188 rows) many times per query. The discriminator: a trivial zero-join single-row INSERT INTO appEvents in the same window also took 12.6s, so this is instance-level CPU/IO contention for ~42s, not query-plan cost on any one statement. Second mechanism for the same root cause: the concurrent render legs did not merely fill the pool, they ran nested-loop-heavy queries in parallel and briefly saturated the instance. CAVEAT for the design, not yet resolved: cutting the connection count does NOT by itself remove the repeated lookupOptions scans. A bundle that still joins lookupOptions per row inside one round-trip has the same plan cost with fewer connections. The scan-reduction payoff has to be designed in - either by resolving lookup labels once per bundle rather than per row, or by keeping labels out of the DB read entirely and mapping them from the already-cached getAllLookupOptions in Node. Worth measuring the two app queries' plans before choosing.
-
Both fan-outs bundled and live. PASS:0016168 deep and PASS:561b380 deep from audit-mars-ca, no findings on either. Shipped: 0016168 v2.77.0 (practica detail, publishes db handoff c8f0013 / migration 121 get_practica_detail_bundle) and 561b380 v2.78.0 (protected layout, publishes db handoff 9140ae1 / migration 122 get_layout_state). Both PTD-confirmed against the remote package.json read at check time; 718 tests green. Concurrency after: /mi-cursada/[id] 4-wide -> 2, /practicas/[id] 6-wide -> 4 (that route was never named in the original report and was the wider of the two), (protected)/layout.tsx 5-wide -> 2. Peak per-render demand no longer exceeds max:6. The photo-nag leg's up-to-3 SEQUENTIAL round trips also fold into the layout row - serial, so it never inflated the concurrency figure, but it ran on every authenticated alumno render and was the instance-load half db-mars-cc measured. Both cached lookup legs deliberately left OUT of both bundles: they resolve through getAllLookupOptions (MARS-658), so folding a warm cache hit into a DB round trip would have made these paths slower, not faster. Design notes worth keeping: getPracticaChangelog split into a pure buildPracticaChangelog so the bundle path and the query path share ONE diff implementation rather than two that agree today; the ladder decision stayed in TS (nagStateFromLayoutColumns, same gates in the same order) so the rungs are defined once. Verification, stated with its own limits: changelog verified byte-identical to to_jsonb(view row) across all 65 columns (the risk the migration header named), reviews and comentarios jsonb-equal to the original queries, pending-photo-request branch 5/5 vs the original TS query, layout scalars 12/12 plus 8/8 on a teacher-only sample added after the first sample turned out to exercise only the false branch of hasTeacherMembership. NOT verified against prod and covered by unit tests ALONE: the exemption and self-delete branches, which have zero rows in the live users table - an equivalence query there returns no-disagreement from an empty set and is indistinguishable from a pass. Not fixed here, split to MARS-674: vPracticaHistorial's repeated lookupOptions Seq Scans. Bundling cannot touch per-call plan cost - the view was already read once per render either way. I originally framed that as something this WI would fix; db-mars-cc corrected it.