fix(db): application_name op elke connectie + begrensde Prisma-pool #115

Merged
janpeter merged 1 commit from fix/db-application-name into main 2026-08-16 20:02:25 +02:00
Owner

Waarom

Op 2026-08-16 liep de gedeelde Postgres drie keer vol. Nadat twee fixes in de web-app waren uitgerold (pool-per-module-scope, en één LISTEN-connectie per SSE-stream) bleef de teller op 86/100 staan, terwijl de web-app er nog maar 3 vasthield.

De meting die overbleef: 64 connecties vanaf het bridge-gateway-adres, met slechts 2 host-sockets naar de container. Ruim zestig kwamen dus ge-NAT binnen — remote MCP-clients over Tailscale. En omdat geen enkele connectie een application_name draagt, was de enige manier om dat vast te stellen NAT-forensiek: host-sockets tellen en aftrekken van wat de database ziet.

Dat is geen werkbare diagnose-route voor een terugkerende storing.

Wat deze PR doet

1. Attributie. Nieuwe src/db-connection.ts levert een application_name afgeleid van S4M_SERVER, S4M_MODEL en — indien aanwezig — SCRUM4ME_WORKER_INSTANCE_ID, bijvoorbeeld s4m-mcp:mac:claude of s4m-mcp:scrum4me-server:codex:idea-51. Ontbrekende identiteit wordt expliciet unknown-host/unknown-model in plaats van leeg, zodat een niet-gelabelde connectie te onderscheiden blijft van een verkeerd geconfigureerde. De naam wordt afgekapt op 63 bytes, want daar kapt Postgres zelf af.

pg_stat_activity wordt hiermee direct bruikbaar:

SELECT application_name, count(*) FROM pg_stat_activity
 WHERE backend_type = 'client backend' GROUP BY 1 ORDER BY 2 DESC;

2. Begrenzing. src/prisma.ts deed new Pool({ connectionString: url }) zonder max. node-postgres valt dan terug op 10 en negeert de connection_limit uit de URL, want dat is een Prisma-parameter en niet een van pg. Dezelfde fout-klasse is eerder in de web-app gerepareerd; dit is de MCP-helft ervan.

Alle zeven dedicated pg.Client-sites (wait-for-job ×2, worker-heartbeat, update-job-status, presence/worker ×2, queue/listen) plus de pool lopen nu via die module.

application_name gaat als config-veld mee en niet gevouwen in de connection string: die herschrijven zou het wachtwoord kunnen her-encoderen.

Wat deze PR bewust NIET doet

Bij het onderzoek was de vraag of de MCP dezelfde fout heeft als de SSE-routes, waar één gedeelde LISTEN-connectie 27 losse verbindingen verving. Het antwoord is nee, en multiplexen zou hier weinig opleveren.

De structuur lijkt op elkaar — wait_for_job, queue_next en queue_wait_reply openen elk een eigen pg.Client met LISTEN voor de duur van de wacht. Maar het verschil zit in de verdeling: bij de SSE-routes stonden er tientallen streams open binnen één proces, terwijl een MCP-proces in de praktijk één blokkerende wacht tegelijk heeft. Het aantal schaalt dus met het aantal client-processen, niet binnen een proces. Een in-process hub zou vrijwel niets samenvoegen.

Lifecycle is ook in orde en blijft ongewijzigd: elke caller sluit zijn client in een finally, wat ik per caller heb nagelopen. Er is geen lek — alleen lineaire groei met het aantal draaiende clients, en dát is een capaciteits- en configuratievraag, geen codefout.

Verificatie

  • 10 nieuwe tests: samenstelling van de naam, worker-instance-variant, expliciete unknown-markering, whitespace-only identiteit, de 63-byte-grens, connection_limit gehonoreerd, fallbacks bij ontbrekende/niet-positieve/niet-hele/niet-URL-waarden, en beide config-vormen inclusief de eis dat de URL ongewijzigd doorgaat. 10/10 pass.
  • tsc --noEmit: 211 fouten mét én zónder deze wijziging; met regelnummers genormaliseerd zijn de foutverzamelingen identiek.
  • vitest run: 104 falende bestanden mét én zónder; het enige verschil is dat er één bestand bij komt dat slaagt (81 → 82). Geen enkele nieuwe failure.

Die 211 type-fouten en 104 falende bestanden zijn pre-existing in een verse clone: prisma:generate is niet gedraaid, dus de generated client ontbreekt. CI draait tegen een volledige checkout.

Na de uitrol

Deze wijziging landt pas op de vloot na een image-rebuild met cache-bust (update_mcp_worker met MCP_CACHE_BUST), en op de Mac pas nadat die zijn MCP herstart. Tot dan blijven bestaande connecties naamloos — application_name wordt bij het opzetten van de verbinding vastgelegd.

## Waarom Op 2026-08-16 liep de gedeelde Postgres drie keer vol. Nadat twee fixes in de web-app waren uitgerold (pool-per-module-scope, en één LISTEN-connectie per SSE-stream) bleef de teller op **86/100** staan, terwijl de web-app er nog maar **3** vasthield. De meting die overbleef: **64 connecties vanaf het bridge-gateway-adres, met slechts 2 host-sockets naar de container**. Ruim zestig kwamen dus ge-NAT binnen — remote MCP-clients over Tailscale. En omdat geen enkele connectie een `application_name` draagt, was de enige manier om dat vast te stellen NAT-forensiek: host-sockets tellen en aftrekken van wat de database ziet. Dat is geen werkbare diagnose-route voor een terugkerende storing. ## Wat deze PR doet **1. Attributie.** Nieuwe `src/db-connection.ts` levert een `application_name` afgeleid van `S4M_SERVER`, `S4M_MODEL` en — indien aanwezig — `SCRUM4ME_WORKER_INSTANCE_ID`, bijvoorbeeld `s4m-mcp:mac:claude` of `s4m-mcp:scrum4me-server:codex:idea-51`. Ontbrekende identiteit wordt expliciet `unknown-host`/`unknown-model` in plaats van leeg, zodat een niet-gelabelde connectie te onderscheiden blijft van een verkeerd geconfigureerde. De naam wordt afgekapt op 63 bytes, want daar kapt Postgres zelf af. `pg_stat_activity` wordt hiermee direct bruikbaar: ```sql SELECT application_name, count(*) FROM pg_stat_activity WHERE backend_type = 'client backend' GROUP BY 1 ORDER BY 2 DESC; ``` **2. Begrenzing.** `src/prisma.ts` deed `new Pool({ connectionString: url })` **zonder `max`**. node-postgres valt dan terug op 10 en negeert de `connection_limit` uit de URL, want dat is een Prisma-parameter en niet een van pg. Dezelfde fout-klasse is eerder in de web-app gerepareerd; dit is de MCP-helft ervan. Alle **zeven** dedicated `pg.Client`-sites (`wait-for-job` ×2, `worker-heartbeat`, `update-job-status`, `presence/worker` ×2, `queue/listen`) plus de pool lopen nu via die module. `application_name` gaat als **config-veld** mee en niet gevouwen in de connection string: die herschrijven zou het wachtwoord kunnen her-encoderen. ## Wat deze PR bewust NIET doet Bij het onderzoek was de vraag of de MCP dezelfde fout heeft als de SSE-routes, waar één gedeelde LISTEN-connectie 27 losse verbindingen verving. **Het antwoord is nee, en multiplexen zou hier weinig opleveren.** De structuur lijkt op elkaar — `wait_for_job`, `queue_next` en `queue_wait_reply` openen elk een eigen `pg.Client` met `LISTEN` voor de duur van de wacht. Maar het verschil zit in de verdeling: bij de SSE-routes stonden er tientallen streams open **binnen één proces**, terwijl een MCP-proces in de praktijk **één** blokkerende wacht tegelijk heeft. Het aantal schaalt dus met het aantal client-processen, niet binnen een proces. Een in-process hub zou vrijwel niets samenvoegen. Lifecycle is ook in orde en blijft ongewijzigd: elke caller sluit zijn client in een `finally`, wat ik per caller heb nagelopen. Er is geen lek — alleen lineaire groei met het aantal draaiende clients, en dát is een capaciteits- en configuratievraag, geen codefout. ## Verificatie - **10 nieuwe tests**: samenstelling van de naam, worker-instance-variant, expliciete `unknown`-markering, whitespace-only identiteit, de 63-byte-grens, `connection_limit` gehonoreerd, fallbacks bij ontbrekende/niet-positieve/niet-hele/niet-URL-waarden, en beide config-vormen inclusief de eis dat de URL ongewijzigd doorgaat. **10/10 pass.** - `tsc --noEmit`: **211 fouten mét én zónder** deze wijziging; met regelnummers genormaliseerd zijn de foutverzamelingen **identiek**. - `vitest run`: **104 falende bestanden mét én zónder**; het enige verschil is dat er één bestand bij komt dat slaagt (81 → 82). Geen enkele nieuwe failure. Die 211 type-fouten en 104 falende bestanden zijn pre-existing in een verse clone: `prisma:generate` is niet gedraaid, dus de generated client ontbreekt. CI draait tegen een volledige checkout. ## Na de uitrol Deze wijziging landt pas op de vloot na een image-rebuild met cache-bust (`update_mcp_worker` met `MCP_CACHE_BUST`), en op de Mac pas nadat die zijn MCP herstart. Tot dan blijven bestaande connecties naamloos — `application_name` wordt bij het opzetten van de verbinding vastgelegd.
fix(db): application_name op elke connectie + begrensde Prisma-pool
All checks were successful
CI / Verify (pull_request) Successful in 2m0s
3d7e433ab5
Twee losse problemen, beide zichtbaar geworden bij de saturatie van de gedeelde
Postgres op 2026-08-16.

Attributie. De server stond op 100/100 met ~60 connecties vanaf remote hosts
over Tailscale. Docker NAT die verbindingen, dus ze verschijnen allemaal als
het bridge-gateway-adres en dragen geen application_name — de enige manier om
ze toe te wijzen was NAT-forensiek (host-sockets tellen en aftrekken). Elke
connectie die dit proces opent draagt nu een naam afgeleid van S4M_SERVER,
S4M_MODEL en, indien aanwezig, SCRUM4ME_WORKER_INSTANCE_ID.

Begrenzing. src/prisma.ts deed `new Pool({ connectionString })` zonder max.
node-postgres valt dan terug op 10 en negeert de connection_limit uit de URL,
want dat is een Prisma-parameter en niet een van pg. Dezelfde fout is eerder in
de web-app gerepareerd; dit is de MCP-helft ervan.

application_name gaat als config-veld mee en niet in de connection string: die
herschrijven zou het wachtwoord kunnen her-encoderen.

Alle zeven dedicated pg.Client-sites en de pool lopen nu via db-connection.ts.
Het lifecycle-gedrag blijft ongewijzigd — de bounded-wait tools openen nog
steeds een eigen LISTEN-client per wachtoproep en sluiten die in hun finally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
janpeter/scrum4me-mcp!115
No description provided.