feat(uitvraag): de gelijktijdigheidsgrens wordt een wachtrij i.p.v. een zeef - #283
feat(uitvraag): de gelijktijdigheidsgrens wordt een wachtrij i.p.v. een zeef#283mreuvekamp wants to merge 11 commits into
Conversation
…en zeef Een ondernemer met meer aangesloten organisaties dan de gelijktijdigheidsgrens zag alleen de eerste twintig; de rest kreeg "tijdelijk niet beschikbaar" terwijl die organisaties nooit bevraagd zijn. Bevestigd als vaste blinde vlek en niet als wisselende deelverzameling: de merge subscribeerde in één keer op alle substreams (Mutiny's default-concurrency is 128), de enige acquire was `tryAcquire()`, en de volgorde is register-volgorde. Twee lagen lossen dat op: - Per ronde subscribet de merge op maximaal `max-parallel-per-ronde` bevragingen en pakt de volgende zodra er één afgerond is. Die wachtrij zit in de backpressure van Mutiny zelf en kost geen thread en geen permit. - De globale semafoor wacht nu ook: zonder vrije permit wordt er asynchroon gepolld tot `max-wachttijd-ms`, zonder een worker-thread te bezetten, zodat het thread-plafond exact `max-concurrent` blijft. Een verstreken wachtbudget krijgt een eigen woord op de lijn (`NIET_OPGEHAALD` naast OK/FOUT/TIMEOUT) plus een melding die "dit deel ontbreekt nog" zegt in plaats van te suggereren dat de organisatie eruit ligt. Alle GESTART-events gaan vooruit, zodat een wachtende organisatie zichtbaar is als "bevragen…" en niet afwezig. De grens hoeft daarmee niet meer mee te groeien met het aantal organisaties van een ondernemer, dus de handmatige 120 in de demo (compose + ZAD-runbook) is vervallen. Ontwerp en afwegingen: docs/plans/2026-09-03-magazijn-bulkhead-wachtrij.md Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeaHUXXf3otyqemsxmhUen
De eerste ronde-test bewees alleen dat er niets wegvalt, en dat deed het wachtbudget van de globale semafoor ook al: zonder de per-ronde-grens bleef hij groen. De nieuwe test zet het wachtbudget krap ten opzichte van de ronde (50 organisaties van 30 ms bij 5 tegelijk, budget 100 ms), zodat hij alleen slaagt als de merge de wachtenden pas subscribet wanneer er een permit vrij is. Gecontroleerd dat hij zonder `withConcurrency(...)` daadwerkelijk faalt. Verder: de KDoc claimde ten onrechte dat de poll-recursie geen keten opbouwt — die groeit wel, begrensd door wachtbudget ÷ poll-interval; dat staat er nu bij. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeaHUXXf3otyqemsxmhUen
JaCoCo coverage
Files
|
🚀 Preview DeploymentDe preview-omgeving van deze PR: Demo
Berichtenuitvraag
Deze preview wordt opgeruimd zodra de PR sluit. |
demo/meet-fanout.sh met drie rondes tegen de lokale stack, zonder de handmatige gelijktijdigheidsgrens: 41 van 45 en 91 van 100 organisaties, gelijk aan de tabel die eerder mét de knop op 120 gemeten is. Geen enkele NIET_OPGEHAALD, en "compleet" hangt net als voorheen aan de organisatie die niet reageert (10,1 s bij zowel 45 als 100), niet aan het aantal — de wachtrij kost geen meetbare doorlooptijd. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeaHUXXf3otyqemsxmhUen
…ll-keten Drie bevindingen uit de review op deze PR, en twee dingen die alleen documentatie nodig hadden. De permit lekte als een wachtende geannuleerd werd tussen de geslaagde `tryAcquire()` en het aanhaken van de release: Mutiny levert een item niet meer af aan een geannuleerde subscriber, dus de release-tak werd overgeslagen en die permit was permanent kwijt. De poll-lus draagt nu de taak zelf mee, zodat acquire en release weer in één synchroon blok staan — zoals vóór de wachtrij. Een test annuleert nu ook een wáchtende, niet alleen de permit-houder. Het wachtbudget stond met 5 s korter dan de per-magazijn query-timeout van 10 s. Een bevraging die aanklopt terwijl alle permits door trage calls bezet zijn, gaf het dan op vóórdat er ook maar één permit kón vrijkomen — voor haar was de wachtrij alsnog een zeef. Default naar 15 s, en de verhouding wordt bij startup gekruisvalideerd naast de bestaande timeout-invarianten. Het poll-interval schaalt nu met het budget (hooguit 200 stappen), zodat de `Uni`-keten kort blijft ook als een operator het budget optrekt; het plafond kan daardoor naar de vangnet-TTL van de ophaal-lock in plaats van een willekeurige minuut. Verder gedocumenteerd: dat een ronde nu meeschaalt met de fan-out en welke som je naloopt voordat die de ophaal-lock overleeft, en waarom er geen grens op de wachtrij-diepte zit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeaHUXXf3otyqemsxmhUen
Verificatie-sectie liep achter op de tests en commando's zoals ze nu zijn. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeaHUXXf3otyqemsxmhUen
Uit de review op deze PR kwamen drie paden waarlangs een fout in één bevraging de héle ophaalronde omtrok: de substream faalde, de merge annuleerde zijn siblings, en de eind-aggregatie werd nooit gesubscribed — geen slotevent, niets opgeslagen, status BEZIG tot de lock-TTL en dus minutenlang 409 voor de gebruiker. - De wachtstap kon zelf falen. `delayIt()` vertaalt een RejectedExecutionException (dode scheduler bij pod-shutdown) naar een gewone Uni-failure, en die viel buiten élke recover — de recover in de taak dekt alleen wat binnen de taak ontstaat. Er ligt nu een vangnet om de hele bevraging. - `registreerCircuit` stond ná die recover, dus een bug in de fóutregistratie nam de ronde mee. De circuit-melding kan niet meer terugslaan op de bevraging. - De half-open probe lekte bij annulering. Hij wordt door niets anders gewist dan een terminale melding, en het venster groeide met de wachtrij van microseconden naar seconden; een verloren probe sloot dat magazijn tot de herstart uit met een CIRCUIT_OPEN die niets over dát magazijn zei. De melding hangt nu aan de terminatie, idempotent met het normale pad. Verder uit de review: - De per-ronde-grens is een operatie van het bulkhead geworden (`ronde(...)`) in plaats van een los getal dat de aanroeper zelf moet toepassen; een tweede aggregatiepad kan de grens nu niet meer vergeten. - De kruisvalidatie deelt in seconden i.p.v. te vermenigvuldigen naar milliseconden — dezelfde overflow-reden die drie regels hoger al staat — en de melding noemt nu ook het plafond, zodat een te hoge query-timeout niet twee deploys kost om te herkennen. - `max-wachttijd-ms` is env-instelbaar geworden, symmetrisch met MAGAZIJN_QUERY_TIMEOUT_SECONDS: zonder die knop kan een operator die aan de query-timeout draait de pod niet meer aan de praat krijgen. - De weergavenaam wordt nog één keer per ronde opgehaald in plaats van twee keer. - Tests erbij: de kruisvalidatie (falend én exact op de grens), dat de bevraging haar wachtbudget écht volmaakt, annuleren diep in de poll-lus, NIET_OPGEHAALD in de wire-contracttabel, naam-correspondentie tussen de twee status-enums, en de kruis-invariant dat een niet-bereikt magazijn nooit als storing telt. - Comment- en doc-correcties: het thread-plafond is een orde en niet exact (de query-timeout onderbreekt de blokkerende call niet), de eerlijkheidsclaim gold binnen een ronde en niet tussen sessies, de worst-case-som van een ronde mist het wachtbudget niet meer, en de MagazijnClient is niet gegenereerd. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeaHUXXf3otyqemsxmhUen
De gelijktijdigheidsgrens gaat naar 50, zowel globaal als per ophaalronde. De ondernemer met honderd organisaties laat daarmee de wachtrij zien (twee golven) en die met vijfenveertig niet — dat is de grens waarop de demo wil landen. Per-ronde gelijk aan globaal betekent wel dat één ondernemer de volle capaciteit mag pakken en een tweede gelijktijdige ronde meteen wacht. Wil je twee rondes naast elkaar op volle snelheid, dan is max-concurrent de knop die omhoog moet. Een wachtrij is per constructie stil — wie in de rij staat doet niets, en juist dat wil je kunnen zien. Daarom logt de ronde zichzelf: - op INFO, twee regels per ophaalronde, zonder configuratie zichtbaar: "Ophaalronde: 100 bevragingen, 50 tegelijk, 50 in de wachtrij" en "Ophaalronde afgerond in 10014 ms: 100 van 100 organisaties bevraagd (91 geslaagd, 9 niet)". Dat tweede getallenpaar is het bewijs dat niemand wegvalt; loopt het uiteen, dan is dat een bevinding. - op DEBUG, per organisatie: wanneer de wachtrij haar oppakte (gehangen aan de onSubscription van haar substream, precies dat moment) en of ze op een permit moest wachten. De eerste vijftig staan op ~1 ms, de rest schuift op tot 284 ms naarmate er plekken vrijkomen. De permit-regels blijven bij één ronde leeg — binnen een ronde is er per definitie een permit vrij op het moment dat de wachtrij iemand oppakt. Ze komen pas in beeld bij gelijktijdige ophaalrondes. Nagemeten in de demo-stack: 0 regels bij één ronde, zoals gedocumenteerd. Nagemeten of een grotere golf de gesimuleerde magazijnen overvraagt: bij 100 organisaties 91 van 100 in 10,1 s, gelijk aan de meting bij twintig; de ondernemer met vijftien wordt sneller (1,3 s tegen 2,8 s) omdat die nu in één golf past. De cijfers staan in het plan, met de waarschuwing dat een koude eerste ronde (45 van 100) daar niets over zegt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DqwSAsiugVvYkJFws6LMeY
De wachtrij en het doorpagineren raken elkaar op drie plekken: - De demo hoefde de gelijktijdigheidsgrens niet meer op te rekken, dus die knop is uit compose.yaml en de runbook-valkuil weg; de paginagrootte van main blijft er wél staan. - De service kent nu naast het bulkhead ook een `paginaLezer`; de tests die hier bijkwamen bouwen hem mee. - De fan-out-tests stubten per call een nieuw bericht zonder tellers. Met de pagineerlus las die stub eindeloos door tot zijn tijdsbudget op was — nu levert hij één complete pagina, zoals een magazijn dat doet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DqwSAsiugVvYkJFws6LMeY
De oude tabel is van vóór #282: één call per organisatie, en een lege demo. Beide toestanden opnieuw gemeten. Leeg levert dezelfde aantallen als eerder; gevuld kost zes calls per organisatie de trage organisaties hun ronde, en bij honderd organisaties breekt de ronde af op het wegschrijven naar Redis — een bevinding die hier als vervolgwerk staat, niet als meetresultaat. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DqwSAsiugVvYkJFws6LMeY
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DqwSAsiugVvYkJFws6LMeY
ericwout-overheid
left a comment
There was a problem hiding this comment.
NIET_OPGEHAALD staat in geen enkel contract — hoe komt een afnemer het te weten?
Deze PR voegt een nieuwe waarde toe aan de woordenlijst op de SSE-lijn (MagazijnStatus). Een afnemer kan die waarde vandaag nergens vinden: de payload van GET /berichten/_ophalen staat niet in de OpenAPI-spec.
# services/berichtenuitvraag/src/main/resources/openapi/berichtenuitvraag-api.yaml:97
text/event-stream:
schema: { type: string }Dat is een opake string. OK/FOUT/TIMEOUT stonden er dus ook nooit in — deze PR slaat geen nieuw gat, hij maakt een bestaand gat zichtbaar. De feitelijke bron is nu MagazijnEvent.kt plus wat proza in de description van het endpoint, en die proza noemt alleen afgekapt, OPHALEN_GEREED en "de per-magazijn FOUT-events".
Gevolg: zonder gepubliceerde uitbreidbaarheidsregel is elke nieuwe statuswaarde een breaking change voor een afnemer die exhaustief op de bekende waarden matcht.
Onze eigen afnemer leest het al verkeerd
demo/demo-console/src/main/resources/META-INF/resources/berichtenbox.js (regel 183-191) crasht niet — het rendert status letterlijk — maar de slotregel telt via gebeurtenis.mislukt, en die teller bevat NIET_OPGEHAALD: naarVoltooidEvent verhoogt mislukt voor elke MagazijnResult.Failure, ongeacht de fout-status.
De KDoc van MagazijnStatus zegt dat een portaal dit moet tonen als "dit deel ontbreekt nog" en niet als een organisatie die eruit ligt. In de samenvattingsregel gebeurt precies dat laatste. Het onderscheid dat de PR belangrijk noemt, komt bij de afnemer dus niet aan.
Voorstel
| # | Maatregel | Waarom |
|---|---|---|
| 1 | SSE-events als echte schemas in components/schemas (oneOf met event als discriminator), gerefereerd vanaf text/event-stream, inclusief een MagazijnStatus-enum met NIET_OPGEHAALD |
De woordenlijst wordt gepubliceerd contract in plaats van Kotlin-broncode |
| 2 | Uitbreidbaarheidsregel in de description: een onbekende status betekent "niet geleverd, opnieuw proberen kan helpen" en nooit OK |
Maakt toekomstige waarden non-breaking |
| 3 | Guard-test die de enum in de spec vergelijkt met MagazijnStatus.entries |
Zonder die test rot de spec weer weg zoals nu |
| 4 | Eigen nietOpgehaald-teller op ophalen-gereed, of expliciet documenteren dat mislukt hem bevat |
Anders kan een portaal het onderscheid in de slotregel niet maken |
| 5 | berichtenbox.js bijwerken |
Onze referentie-afnemer hoort punt 2 en 4 voor te doen |
Let op dat de bestaande gates dit niet vangen: de Spectral-lint blijft groen bij een toegevoegde enum-waarde, en swagger-request-validator valideert geen SSE-body. Punt 3 is daarom geen luxe.
Vraag
Doen we 1 t/m 5 in deze PR, of wordt het een apart issue onder #349 en houdt deze PR zich bij de wachtrij? Dat tweede is inhoudelijk schoner — het gat is ouder dan deze wijziging — maar dan gaat NIET_OPGEHAALD wel live als niet-gedocumenteerde waarde. Punt 4 en 5 raken wél rechtstreeks het gedrag dat deze PR introduceert; die zou ik hoe dan ook hier oppakken.
Issue: MinBZK/MijnOverheidZakelijk#1038
Wat er aan de hand was
Een ondernemer met meer aangesloten organisaties dan de gelijktijdigheidsgrens zag alleen de eerste
twintig; de rest kreeg "tijdelijk niet beschikbaar" terwijl die organisaties nooit bevraagd zijn.
Het issue vroeg zich af of het steeds dezelfde organisaties treft. Dat is nu bevestigd: het is een
vaste blinde vlek. Drie dingen werkten samen:
Multi.createBy().merging()heeft een default-concurrency van 128 (nagekeken in Mutiny3.3.0), dus bij honderd organisaties subscribeerde de merge in één keer op alle honderd
substreams — honderd permit-pogingen tegen twintig permits.
tryAcquire(): geen permit betekende onmiddellijkOVERBELAST.associate→LinkedHashMap, register-volgorde), dus de eerstetwintig wonnen elke ronde.
Wat er nu gebeurt
Laag 1 — de ronde wacht op zichzelf.
withConcurrency(max-parallel-per-ronde)op de merge: erzijn hooguit zoveel bevragingen onderweg en de volgende wordt gepakt zodra er één afgerond is. Die
wachtrij zit in de backpressure van Mutiny zelf — geen extra primitief, geen thread en geen permit
voor een wachtende organisatie. Hiermee verdwijnt de blinde vlek volledig.
Laag 2 — de globale grens wacht ook. De semafoor is gedeeld over sessies, dus zonder ingreep
zou hetzelfde patroon over sessies heen terugkomen. Zonder vrije permit wordt er nu asynchroon
gepolld (25 ms) tot
max-wachttijd-ms, zonder een worker-thread te bezetten — het thread-plafondblijft exact
max-concurrent. Waarom pollen en geen FIFO-wachtrij van emitters staat in het plan:een permit-overdragende wachtrij heeft een race die niet lokaal te sluiten is (een permit toekennen
aan een wachtende die net zijn budget overschreed, lekt hem).
Een verstreken wachtbudget is geen storing. Nieuwe status
NIET_OPGEHAALDnaastOK/FOUT/TIMEOUT, met de melding "Nog niet opgehaald: te veel organisaties tegelijk inbehandeling". Alle GESTART-events gaan vooruit, zodat een wachtende organisatie zichtbaar is als
"bevragen…" en niet afwezig.
Knoppen
…magazijn-bulkhead.max-concurrent…magazijn-bulkhead.max-parallel-per-ronde…magazijn-bulkhead.max-wachttijd-msFail-fast gevalideerd bij boot: alle drie > 0,
max-parallel-per-ronde ≤ max-concurrent,max-wachttijd-ms ≤ 120000en — gekruisvalideerd invalideerTimeouts()naast de bestaandetimeout-invarianten —
max-wachttijd-ms ≥ magazijn-query-timeout-seconds × 1000. De grens hoeftniet meer mee te groeien met het aantal organisaties van een ondernemer, dus de handmatige 120 in de
demo (
compose.yaml+ het ZAD-runbook) is vervallen.Wat de review op deze PR opleverde
Drie dingen aangepast, twee gedocumenteerd:
tryAcquire()in de poll-stapen het aanhaken van de release in de mapper erna zat een operator-grens; Mutiny levert een item
niet meer af aan een geannuleerde subscriber, dus de release-tak werd dan overgeslagen en die
permit was permanent kwijt (geen reaper). De poll-lus draagt de taak nu zelf mee, zodat acquire
en release weer in één synchroon blok staan. Een test annuleert nu ook een wachtende.
die aanklopt terwijl alle permits door trage calls bezet zijn, gaf het dan op vóórdat er één
permit kón vrijkomen. Default naar 15 s plus de kruisvalidatie hierboven.
Uni-keten kortblijft als een operator het budget optrekt.
⌈organisaties ÷ max-parallel-per-ronde⌉ × query-timeoutin plaats van ongeveer éénquery-timeout. Bij honderd organisaties is dat 50 s, ruim binnen de
PT2Mvanaggregation-lock-ttl; voorbij ~240 organisaties zou die lock middenin een ronde verlopen. Derekensom staat in de operator-handleiding — een startup-controle kan het niet, want het aantal
organisaties is bij boot niet bekend.
begrensd, en het aantal volgt uit
max-parallel-per-ronde× gelijktijdige rondes).Doorlooptijd
Een permit komt vrij zodra een wíllekeurige call termineert, niet pas als de traagste klaar is. In
de gemeten verdeling (91 van 100 antwoorden binnen ~100 ms, enkele op de query-timeout van 10 s)
houden de trage calls een handvol permits bezet terwijl de rest de snelle calls in een fractie van
een seconde afwerkt. "Compleet" blijft dus bepaald door de organisatie die niet reageert — precies
zoals nu.
Tests
MagazijnAggregatieBulkheadTest: wachten tot een permit vrijkomt, verstreken budget → deverlopen-tak, géén permit-lek na een verstreken budget, "alle wachtenden komen aan de beurt",
"nooit meer taken tegelijk dan permits", en de config-validatie inclusief de nieuwe invariant.
BerichtensessiecacheServiceTest:@ParameterizedTestmet 5, 6 en 50 organisaties tegen eengrens van 5 — alle organisaties krijgen een GESTART- én een geslaagd VOLTOOID-event, geen enkele
mislukking, en alle GESTART-events komen vóór de eerste uitkomst. Dat is het acceptatiecriterium
"geen enkele organisatie valt structureel buiten beeld"; vóór deze wijziging faalt die test met
45 afwijzingen.
./mvnw clean verify -pl services/berichtenuitvraag,libraries/fbs-berichtensessiecache -am:groen, JaCoCo-gate gehaald (de bulkhead 41/41 regels), detekt 0 bevindingen, geen nieuwe
build-warnings.
shellcheck -x -S warning demo/smoke.shschoon.De meting
demo/meet-fanout.sh 3tegen de lokale demo-stack, op de standaardinstellingen (de handmatige120 is weg). Mediaan over drie rondes:
Vóór deze wijziging gaf diezelfde standaardinstelling 20 van 45 en 20 van 100. Nu komen de aantallen
exact uit op de tabel van MinBZK/MijnOverheidZakelijk#1012, die mét de knop op 120 gemeten is — de
knop is dus overbodig geworden en niet vervangen door verlies. Wat niet slaagde is wat de simulator
opzettelijk stuk zet (33 FOUT en 6 TIMEOUT over alle rondes samen, geen enkele
NIET_OPGEHAALD). "Compleet" hangt net als voorheen aan de organisatie die niet reageert: 45 en100 komen allebei op 10,1 s uit, dus de wachtrij kost geen meetbare doorlooptijd. In de ronde met
honderd organisaties staan alle 100 GESTART-events vóór het eerste VOLTOOID-event.
Opnieuw gemeten na het samenvoegen met #282
De tabel hierboven is van vóór het doorpagineren: één call per organisatie, en een lege demo — elke
organisatie antwoordde met nul berichten. Nu staat de demo op pagina's van vijf en zet de
standaardvulling 27 berichten per organisatie, dus zes opeenvolgende calls per organisatie. Beide
toestanden opnieuw gemeten, drie ronden, mediaan.
Lege magazijnen — vergelijkbaar met de tabel hierboven, en dezelfde aantallen:
Gevulde magazijnen — de normale demo-toestand:
Nog steeds geen enkele
NIET_OPGEHAALD: de wachtrij is niet de bottleneck. Twee dingen zijn wélanders, allebei van het doorpagineren:
gaat het van 15 van 15 in 2,8 s naar 13 van 15 op de query-timeout; de twee die afvallen zijn
TIMEOUT en geen FOUT. Op deze opstelling draait één simulator alle 98 magazijnen op één
PostgreSQL, dus dit hangt aan de demo-instelling (
pageSize5) en aan de opstelling.hieronder — dat is een bevinding, geen meetresultaat.
Bevinding: de cache-schrijf schaalt niet mee met de fan-out
Alle honderd organisaties worden bevraagd en alle VOLTOOID-events komen door; daarna faalt
RedisBerichtenCache.storemetRedis waiting queue is fullen eindigt de SSE-stroom zonderophalen-gereed. Twee van de drie ronden liepen zo.storezet per bericht twee commando's (HSET+EXPIRE) in één transactie en biedt ze in één keeraan (
Uni.join().all). Honderd organisaties × 27 berichten is ruim 4300 commando's tegelijk, tegende 2048 die de Vert.x-Redis-client in zijn wachtrij toelaat (
quarkus.redis.max-waiting-handlers).Gemeten: 945 berichten (45 organisaties) gaat altijd goed, 2100–2300 berichten (100 organisaties)
tweemaal van de drie mis — een race, want de client verwerkt tijdens het aanbieden ook al.
max-waiting-handlersis een client-instelling en geldt in productie net zo goed, dus dit is geendemo-eigenschap. Deze PR is wél wat het zichtbaar maakt: eerder kwamen er maar twintig organisaties
door, en die pasten er ruim binnen. Twee richtingen — de knop verhogen (verplaatst de grens) of
storein delen aanbieden (houdt hem onafhankelijk van de fan-out, raakt de transactie-semantiek) —en geen van beide zit in deze PR.
Tweede reviewronde (vijf agents op de diff)
Drie manieren waarop één lokale fout de héle ophaalronde kon meenemen — substream faalt, merge
annuleert siblings, eind-aggregatie nooit gesubscribed, dus geen slotevent en status
BEZIGtot delock-TTL:
delayIt()maakt van eenRejectedExecutionException(dodescheduler bij pod-shutdown) een gewone
Uni-failure, en die viel buiten élke recover — derecover in de taak dekt alleen wat binnen de taak ontstaat. Er ligt nu een vangnet om de hele
bevraging.
registreerCircuitstond ná die recover, dus een bug in de fóutregistratie nam de ronde mee.melding, en het venster groeide met de wachtrij van microseconden naar seconden; een verloren
probe sloot dat magazijn tot de herstart uit met een
CIRCUIT_OPENdie niets over dát magazijnzei. De melding hangt nu aan de terminatie.
Verder: de per-ronde-grens is een operatie van het bulkhead geworden (
ronde(...)) zodat een tweedeaggregatiepad hem niet kan vergeten; de kruisvalidatie deelt in seconden in plaats van te
vermenigvuldigen (dezelfde overflow-reden die drie regels hoger al stond) en noemt nu ook het
plafond;
max-wachttijd-msis env-instelbaar geworden, symmetrisch metMAGAZIJN_QUERY_TIMEOUT_SECONDS. Zes tests erbij, plus comment- en doc-correcties (hetthread-plafond is een orde en niet exact, de eerlijkheidsclaim gold binnen een ronde en niet tussen
sessies, en de worst-case-som van een ronde mist het wachtbudget niet meer — die valt daarmee op
125 s in plaats van 50 s, nét buiten de
PT2Mvan de ophaal-lock).Bewust niet gedaan, met reden:
geslaagd/mislukt. Een verstreken wachtbudget telt nu alsmislukt,waardoor
logboekStatusVoorde verwerking in het Logboek Dataverwerkingen opERRORzet —strikt genomen onjuist, want capaciteitsbeleid is geen verwerkingsfout. Een derde teller raakt
echter het SSE-contract, de aggregatiestatus in Redis,
demo/meet-fanout.sh(die de tellingenkruiscontroleert) en de berichtenbox-weergave, voor een toestand die alleen bij aanhoudende
verzadiging over sessies heen voorkomt. Losgetrokken als vervolgwerk.
valideerTimeouts()en niet in zijn eigeninit. Alternatief zou een tweede lezer vanmagazijn-query-timeout-secondsmaken terwijl álle timeout-ordening van deze service nu op éénplek staat. Prijs: één getter naar buiten.
Wat nog open staat
Ophalen breekt vlak voor de finish af bij een ondernemer met veel aangesloten organisaties MijnOverheidZakelijk#1077. Zolang die er staat, breekt de ophaalronde van de grootste
persona op een gevulde demo af zonder slotevent.
verify-zad.md), zoals #1012 dat ookdeed. De cijfers hierboven komen van één machine waarop de hele stack draaide.
NIET_OPGEHAALDis een toevoeging aan de SSE-stroom die in geen API-contract staat. Dedemo-console rendert onbekende statussen netjes (
status+foutmelding); de berichtenbox uit deproeftuin is niet nagekeken. De status treedt alleen op bij aanhoudende verzadiging over sessies
heen, dus in de demo hoort hij niet voor te komen.
BERICHTENSESSIECACHE_MAGAZIJN_BULKHEAD_MAX_CONCURRENT=120op het ZAD-componentuitvraagmoet met de hand weg (zadctl env unset); die staat in de OM-projectspec en niet indeze repo. Het runbook zegt het nu ook.
🤖 Generated with Claude Code
https://claude.ai/code/session_01HeaHUXXf3otyqemsxmhUen