Nachtraeglich behobener, in 10-observability-tracing.md urspruenglich nur als offener
Nebenbefund dokumentierter Bug - plus ein zweiter, strukturell identischer Bug, der erst bei
der realen Verifikation dieses Fixes auffiel.
PolicyManagementService.activatePolicy ruft synchron
PolicyEventPublisherPort.publish(...) (KafkaPolicyEventPublisherAdapter) auf. Ohne
erreichbaren Kafka-Broker blockierte KafkaTemplate.send() den aufrufenden HTTP-Request-
Thread bis zu max.block.ms (Kafka-Default ca. 60s) - ein eigentlich erfolgreicher
Aktivierungs-Request liess den Client minutenlang haengen statt schnell zu antworten oder
schnell zu scheitern.
KafkaTemplate.send() gibt zwar ein asynchrones CompletableFuture zurueck, blockiert aber
selbst INTERN (Metadaten-/Cluster-Abruf fuer die Zielpartition), bevor es dieses Future
ueberhaupt zurueckgibt - die Asynchronitaet beginnt erst NACH diesem blockierenden Schritt,
nicht davor. Ein nicht erreichbarer Broker verzoegert deshalb bereits den send()-Aufruf
selbst.
AsyncConfig: dedizierter, bewusst klein gehaltener ThreadPoolTaskExecutor
(policyEventExecutor, 2-4 Threads, begrenzte Queue mit CallerRunsPolicy als
Rueckdruck-Ventil) statt Spring Boots implizitem, unbegrenztem
SimpleAsyncTaskExecutor-Fallback.KafkaPolicyEventPublisherAdapter.publish: @Async("policyEventExecutor") - der komplette
Aufruf inklusive Metadaten-Abruf laeuft jetzt auf dem dedizierten Executor, nicht mehr auf
dem HTTP-Request-Thread.application.yml: spring.kafka.producer.properties.max.block.ms: 5000 als Obergrenze
(statt Kafka-Default ca. 60s) - verhindert, dass der begrenzte Executor-Threadpool bei
Dauerausfall durch haengende Tasks erschoepft wird.Beim ersten Testlauf des Fixes zeigte sich: der HTTP-Thread war zwar entkoppelt (Ziel
erreicht), aber der Hintergrund-Task auf policyEventExecutor brauchte trotzdem ca. 60
Sekunden, bevor die Veroeffentlichung als fehlgeschlagen erkannt wurde - nicht die
konfigurierten 5 Sekunden. Ursache: Micrometers Kafka-Observation-Instrumentierung (siehe
10-observability-tracing.md) laesst KafkaTemplate.send() zusaetzlich die Cluster-ID ueber
einen KafkaAdmin-Aufruf abfragen - dessen eigene Default-Timeouts sind von
max.block.ms unabhaengig. Behoben durch zusaetzliches, ebenfalls auf 5s begrenztes
spring.kafka.admin.properties (default.api.timeout.ms, request.timeout.ms,
socket.connection.setup.timeout.ms/.max.ms).
Die urspruengliche Fix-Version fing Fehlschlaege nur ueber .exceptionally(...) auf dem von
send() zurueckgegebenen CompletableFuture ab. Real beobachtet: der oben beschriebene
KafkaAdmin-Fehlschlag wirft aber SYNCHRON (bevor send() ueberhaupt ein Future
zurueckgibt) - ohne try/catch um den gesamten send()-Aufruf entkam diese Exception der
eigenen Fehlerbehandlung und wurde stattdessen von Springs generischem
SimpleAsyncUncaughtExceptionHandler mit einer weniger aussagekraeftigen Meldung geloggt.
Behoben durch zusaetzliches try/catch um den kompletten send()-Aufruf.
Boot der echten Anwendung (nicht nur ein isolierter Test) gegen eine per RFC 5737 garantiert
nicht existierende Broker-Adresse (203.0.113.1:9999, TEST-NET-3 - haengt bei
TCP-Verbindungsversuchen tatsaechlich, anders als ein sofort ablehnender geschlossener
lokaler Port):
POST /api/policies/{policyNumber}/activate: 0,29s, HTTP 200 (vorher: haengender
Request bis zu ~60s).KafkaPolicyEventPublisherAdapter-eigene Warnmeldung mit Policennummer und Kontext - nicht
mehr Springs generischer Handler.Zusaetzlich ein Zusatztest (KafkaPolicyEventPublisherAdapterBlockingBugTest) gegen dieselbe
TEST-NET-3-Adresse: misst die Laufzeit des publish()-Aufrufs selbst und schlaegt fehl,
falls sie 3s ueberschreitet.
createPolicy fuer 30 SekundencreatePolicy (ueber PremiumCalculationService.lookupHistoricalBenchmark ->
Db2LegacyBenchmarkPort.lookupAveragePremium) ruft synchron db2DataSource.getConnection()
auf. Ohne explizites connectionTimeout verwendet HikariCP seinen Default von 30 Sekunden -
real gemessen: 30,76s pro createPolicy-Request, sobald DB2 nicht erreichbar ist (in
dieser Umgebung immer, siehe 09-offene-punkte-geschlossen.md). Das widerspricht dem
eigenen Design-Anspruch: der historische Benchmark ist explizit als optionale,
tolerierbare Anreicherung dokumentiert (PremiumCalculationService.lookupHistoricalBenchmark
faengt UpstreamSystemUnavailableException bereits ab) - ein 30-Sekunden-Fehlschlag fuer ein
"nice to have" ist offensichtlich unangemessen.
Db2DataSourceConfig.db2DataSource: config.setConnectionTimeout(3_000) - explizite,
kurze Obergrenze statt des 30s-Defaults. Bewusst kein @Async hier (anders als bei Bug 1):
der Benchmark-Wert wird synchron fuer die Praemienberechnung gebraucht, ein asynchroner
Aufruf wuerde die Fachlogik unnoetig verkomplizieren - eine kurze, bounded Wartezeit ist hier
der richtige Kompromiss.
POST /api/policies gegen nicht erreichbares DB2: 3,89s statt zuvor 30,76s, weiterhin
HTTP 201 mit korrekter Praemie (ohne historische Korrektur, wie vor dem Fix bereits
vorgesehen).
Beide Bugs sind dieselbe Fehlerklasse: ein als "optional"/"tolerierbarer Ausfall" dokumentierter externer Aufruf (Kafka-Event, DB2-Benchmark) nutzte trotzdem einen Kafka-/JDBC-Client-Default-Timeout, der fuer einen KRITISCHEN, nicht fuer einen optionalen Aufruf gedacht ist (60s bzw. 30s). "Optional/tolerierbar" muss sich auch in der Timeout-Konfiguration widerspiegeln, nicht nur im Exception-Handling - sonst bleibt der eigentliche Blockierungseffekt trotz korrekt behandelter Exception bestehen. Beide Funde entstanden nicht durch Codelese, sondern durch echtes End-to-End-Timing reale Requests gegen absichtlich unerreichbare Zielsysteme - Konfigurationsprobleme dieser Art sind im Code selbst meist nicht sichtbar.
⌂ Cockpit