fix(hub): één vervanger per storing voor de question-listener #173

Merged
janpeter merged 1 commit from fix/hub-question-listener-generation into main 2026-08-16 21:08:43 +02:00
Owner

Waarom

Een pg.Client emit bij één beëindiging zowel 'error' als 'end'. In startQuestionListener waren die allebei aan reconnect() gekoppeld, zonder guard en zonder de voorganger te sluiten:

client.on('error', (err) => {  reconnect() })
client.on('end', reconnect)          // ← dezelfde storing, tweede reconnect

Elke storing leverde dus twee levende vervangers op, en die verdubbelden bij de volgende storing opnieuw.

Dit lag te slapen zolang niets idle verbindingen opruimde. Sinds idle_session_timeout op de database staat wordt de listener elke 15 minuten gereapt, en groeide het in golven door. Gemeten op prod: 27 gelijktijdige LISTEN scrum4me_changes-connecties uit één web-proces, stabiel, terwijl de multiplexer van #172 er één hoort te houden. Het log bevestigt het mechanisme — 45× "idle-session timeout" naast 48× "Connection terminated unexpectedly", dus twee events per beëindiging, in foutgolven op precies 15 minuten afstand.

Een herstart van de web-app zette het terug op 1, wat de diagnose sluit: het is accumulatie, geen momentopname.

Wat deze PR doet

Hetzelfde patroon als de queue-listener-fix in #172 — voorganger sluiten plus een generatieteller — en aanvullend een scheduled-vlag per generatie.

Die vlag is nodig omdat de generatieteller alléén een gat laat: vuurt dezelfde client twee keer vóórdat de vervanger daadwerkelijk draait, dan is de generatie nog ongewijzigd en passeren beide events de check. Dat gat zat ook nog in de queue-listener uit #172 en is hier meteen meegenomen.

Verificatie

Drie regressietests, met positieve controle:

test op de fix op de ongewijzigde code
één storing → één vervanger pass FAILexpected [ …(3) ] to have a length of 2
voorganger wordt gesloten pass FAILexpected false to be true
laat event van een vervangen client wordt genegeerd pass FAIL — 3 in plaats van 2

Ze falen dus aantoonbaar zonder de fix; een regressietest die ook op de oude code slaagt bewaakt niets.

Verder: npm run typecheck 296 fouten mét én zónder, met genormaliseerde regelnummers identieke verzameling. npm test 17 falende regels mét én zónder — niets nieuw, niets stilletjes opgelost. Beide getallen zijn pre-existing in de dev-clone (stale generated client / dirty submodule). eslint op het gewijzigde bestand: exit 0.

Kanttekening bij mijn eigen eerdere inschatting

In #172 schreef ik over deze listener: "functioneel zelfherstellend, maar onnodige churn — kandidaat voor een volgende PR." Dat was te mild. Het is geen churn maar vermenigvuldiging, en de idle_session_timeout die ik dezelfde dag zette was er de trigger voor.

## Waarom Een `pg.Client` emit bij **één** beëindiging zowel `'error'` als `'end'`. In `startQuestionListener` waren die allebei aan `reconnect()` gekoppeld, zonder guard en zonder de voorganger te sluiten: ```js client.on('error', (err) => { … reconnect() }) client.on('end', reconnect) // ← dezelfde storing, tweede reconnect ``` Elke storing leverde dus **twee** levende vervangers op, en die verdubbelden bij de volgende storing opnieuw. Dit lag te slapen zolang niets idle verbindingen opruimde. Sinds `idle_session_timeout` op de database staat wordt de listener elke 15 minuten gereapt, en groeide het in golven door. Gemeten op prod: **27 gelijktijdige `LISTEN scrum4me_changes`-connecties uit één web-proces**, stabiel, terwijl de multiplexer van #172 er één hoort te houden. Het log bevestigt het mechanisme — **45× "idle-session timeout" naast 48× "Connection terminated unexpectedly"**, dus twee events per beëindiging, in foutgolven op precies 15 minuten afstand. Een herstart van de web-app zette het terug op 1, wat de diagnose sluit: het is accumulatie, geen momentopname. ## Wat deze PR doet Hetzelfde patroon als de queue-listener-fix in #172 — voorganger sluiten plus een generatieteller — en aanvullend een `scheduled`-vlag per generatie. Die vlag is nodig omdat de generatieteller alléén een gat laat: vuurt dezelfde client twee keer vóórdat de vervanger daadwerkelijk draait, dan is de generatie nog ongewijzigd en passeren beide events de check. **Dat gat zat ook nog in de queue-listener uit #172** en is hier meteen meegenomen. ## Verificatie Drie regressietests, met positieve controle: | test | op de fix | op de ongewijzigde code | |---|---|---| | één storing → één vervanger | pass | **FAIL** — `expected [ …(3) ] to have a length of 2` | | voorganger wordt gesloten | pass | **FAIL** — `expected false to be true` | | laat event van een vervangen client wordt genegeerd | pass | **FAIL** — 3 in plaats van 2 | Ze falen dus aantoonbaar zonder de fix; een regressietest die ook op de oude code slaagt bewaakt niets. Verder: `npm run typecheck` **296 fouten mét én zónder**, met genormaliseerde regelnummers identieke verzameling. `npm test` **17 falende regels mét én zónder** — niets nieuw, niets stilletjes opgelost. Beide getallen zijn pre-existing in de dev-clone (stale generated client / dirty submodule). `eslint` op het gewijzigde bestand: exit 0. ## Kanttekening bij mijn eigen eerdere inschatting In #172 schreef ik over deze listener: "functioneel zelfherstellend, maar onnodige churn — kandidaat voor een volgende PR." Dat was te mild. Het is geen churn maar vermenigvuldiging, en de `idle_session_timeout` die ik dezelfde dag zette was er de trigger voor.
fix(hub): één vervanger per storing voor de question-listener
All checks were successful
CI / Lint, Typecheck, Test & Build (pull_request) Successful in 4m21s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
434ab79ff5
Een pg.Client emit bij één beëindiging zowel 'error' als 'end'. Beide waren aan
reconnect() gekoppeld, zonder guard en zonder de voorganger te sluiten, dus elke
storing leverde TWEE levende vervangers op. Die verdubbelden bij de volgende
storing opnieuw.

Dat lag te slapen zolang niets idle verbindingen opruimde. Sinds
idle_session_timeout op de database staat wordt de listener elke 15 minuten
gereapt, en groeide het in golven door: gemeten 27 gelijktijdige LISTEN-
connecties op scrum4me_changes uit één web-proces, met 45 idle-session-timeouts
naast 48 'Connection terminated unexpectedly' in het log — twee events per
beëindiging, precies het mechanisme.

Zelfde patroon als de queue-listener-fix in #172: voorganger sluiten plus een
generatieteller. Aanvullend een scheduled-vlag per generatie, want de
generatieteller alleen dekt het geval niet dat dezelfde client twee keer meldt
vóór de vervanger draait — dat gat zat ook nog in de queue-listener en is hier
meteen meegenomen.

De drie regressietests falen aantoonbaar op de ongewijzigde code (3 clients in
plaats van 2, en de voorganger niet gesloten).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
s4m-codex-reviewer left a comment

Verdict: APPROVED

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • Geen blokkerende of error-severity findings gevonden.

Reviewnotities

De wijziging past bij de hub-runtime ontwerp-eis uit de communicatiecentrum-spec: één in-proces runtime met eigen reconnect/backoff en graceful shutdown. De nieuwe questionListenerGeneration plus lokale scheduled-guard voorkomt dubbele vervangers wanneer één pg-client zowel error als end emit, en sluit de vorige LISTEN-client expliciet af voordat een vervanger actief wordt. De queue-listener krijgt dezelfde dedupe tegen herhaalde error-events binnen dezelfde generatie.

De toegevoegde Vitest-regressietest dekt de kernscenario's: één vervanger bij error+end, afsluiten van de voorganger, en late events van een vervangen client. Ik kon geen lokale test run uitvoeren omdat de workspace geen checkout bevatte, maar de aangeleverde diff is coherent en plan-onafhankelijk beoordeelbaar.

# Verdict: APPROVED Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - Geen blokkerende of error-severity findings gevonden. ## Reviewnotities De wijziging past bij de hub-runtime ontwerp-eis uit de communicatiecentrum-spec: één in-proces runtime met eigen reconnect/backoff en graceful shutdown. De nieuwe `questionListenerGeneration` plus lokale `scheduled`-guard voorkomt dubbele vervangers wanneer één pg-client zowel `error` als `end` emit, en sluit de vorige LISTEN-client expliciet af voordat een vervanger actief wordt. De queue-listener krijgt dezelfde dedupe tegen herhaalde error-events binnen dezelfde generatie. De toegevoegde Vitest-regressietest dekt de kernscenario's: één vervanger bij `error`+`end`, afsluiten van de voorganger, en late events van een vervangen client. Ik kon geen lokale test run uitvoeren omdat de workspace geen checkout bevatte, maar de aangeleverde diff is coherent en plan-onafhankelijk beoordeelbaar.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
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!173
No description provided.