← Zur Uebersicht

Zwei blockierende Bugs behoben: Kafka-Publish und DB2-Benchmark-Lookup

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.

Bug 1: Kafka-Event-Publish blockierte den HTTP-Thread

Symptom

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.

Warum das so passiert

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.

Fix

Zweiter realer Fund waehrend der Fix-Verifikation: KafkaAdmin-Timeout unbounded

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).

Dritter realer Fund: Exceptions entkamen der eigenen Fehlerbehandlung

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.

Wie real verifiziert

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):

Zusaetzlich ein Zusatztest (KafkaPolicyEventPublisherAdapterBlockingBugTest) gegen dieselbe TEST-NET-3-Adresse: misst die Laufzeit des publish()-Aufrufs selbst und schlaegt fehl, falls sie 3s ueberschreitet.

Bug 2: DB2-Benchmark-Lookup blockierte createPolicy fuer 30 Sekunden

Symptom, real erst bei der Verifikation von Bug 1 entdeckt

createPolicy (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.

Fix

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.

Wie real verifiziert

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).

Gemeinsame Lehre

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