Caluga - Java blog

Morphium 6.2.2 — Bugfix-Release mit PoppyDB-Härtung und Performance

Morphium 6.2.2 — Bugfix-Release mit PoppyDB-Härtung und Performance

Nur zwei Wochen nach 6.2.0 ist Morphium 6.2.2 da. Kein großes Feature-Release diesmal — stattdessen ein konzentrierter Bugfix-Sprint, der vor allem PoppyDB deutlich stabiler und schneller macht. Und ein paar Dinge, die mir schon länger auf den Nägeln brannten.

PoppyDB: Von "funktioniert meistens" zu "funktioniert"

PoppyDB hat in 6.2.0 seinen eigenen Namen und sein eigenes Maven-Modul bekommen. In 6.2.2 bekommt es die Zuverlässigkeit, die es verdient. Fast 20 Fixes betreffen den Server — hier die wichtigsten:

Wire Protocol Corruption gefixt

Das war der übelste Bug: Wenn ein Change-Stream-Event eintraf, während der Netty I/O-Thread gerade eine andere Antwort auf derselben Connection schrieb, wurden die Bytes interleaved. Das Resultat: Illegal opcode 0, korrupte Messages, und Clients die sich nicht mehr erholen konnten.

Die Ursache: CompletableFuture.whenComplete() schrieb direkt auf den Netty-Channel aus einem Background-Thread. Fix: Responses werden jetzt über ctx.channel().eventLoop().execute() zurück auf den Event-Loop-Thread dispatcht. Serialisiert alle Writes pro Connection, wie es sich für Netty gehört.

Gleiches Problem existierte Client-seitig: SingleMongoConnection.sendQuery() war nicht synchronized — bei shared Connections von mehreren Threads kamen die Bytes durcheinander. Jetzt sind sendQuery, sendCommand und sendAndWaitForReply synchronisiert.

Thread-Leak: 20.000 Threads pro Node

Wenn Clients ohne killCursors disconnecteten (was jeder MongoDB-Driver tut), blieben Watch-Cursors und Change-Stream-Subscriptions ewig am Leben. Jeder verwaiste Cursor blockierte einen Thread im Executor-Pool und akkumulierte Virtual Threads für Event-Dispatch. Unter Testlast: über 20.000 Threads pro PoppyDB-Node, OOM.

Fix: Cursor-IDs werden jetzt pro Netty-Channel getrackt und in channelInactive() automatisch gekillt.

Raft Election Stabilität

Drei PoppyDB-Nodes auf dem gleichen Host mit gleicher Priority führten zu endlosen Split-Vote-Elections. Dazu kam: onLeaderDiscovered feuerte bei jedem Heartbeat statt nur bei Leader-Wechseln, und isLeader()/getCurrentLeader() in getHelloResult() waren nicht atomar — der PooledDriver sah primäre Flapping-Events mehrmals pro Sekunde.

Fix: Generation Guard für Election-Timer, atomare Leader-Snapshots, und Nodes sollten unterschiedliche Priorities verwenden.

Weitere PoppyDB-Fixes

  • Update-Responses: "matched" statt dem MongoDB-Standard "n" → alle Updates schlugen fehl
  • Find-Cursor-Batching: Alle Dokumente in einer Antwort statt batched → Iteratoren kaputt
  • Upsert-Count: n: 0 statt n: 1 bei Upserts → storeMap() Assertions schlugen fehl
  • writeErrors nicht weitergeleitet: Duplicate-Key-Errors beim Upsert wurden verschluckt
  • Hostname 0.0.0.0: Hello-Response mit Bind-Adresse statt Hostname → Clients konnten nicht verbinden
  • Tailable Cursors: Direct-Insert-Path hat notifyTailableCursors nicht aufgerufen

Performance: 4.7x schnellerer Start

ClassGraphCache

Jedes new Morphium() hat bisher 2-4 ClassGraph-Classpath-Scans ausgelöst — jeweils 100-500ms. Bei Tests mit vielen Morphium-Instanzen dominierte das die Startzeit. Jetzt gibt es einen JVM-weiten Singleton-Cache: der erste Scan wird gecacht, alle weiteren Instanzen nutzen das Ergebnis. In den Tests: BasicFunctionalityTest von 67s auf 14s.

Weitere Performance-Verbesserungen

  • Zero-Copy BSON Decoder — weniger Allokationen pro Dokument
  • Shallow Copy statt Deep Copy für Change-Stream-Events
  • Direct Dispatch für Hot-Path-Commands (insert, update, delete, find, count)
  • PoppyDB 3x schneller als MongoDB für Einzeloperationen in lokalen Benchmarks (insert 0.74ms vs 4.48ms)

Wichtige Bugfixes im Core

Change-Stream-Events nach Collection-Drop verloren

Mehrere Race Conditions im InMemoryDriver konnten dazu führen, dass Events nach einem drop() verloren gingen oder dupliziert wurden. Fix: Sequence-Counter wird beim Drop um 100 vorgerückt, stale Events werden gefiltert, und die History wird vor und nach der Drop-Notification gepurgt.

Network-Retry auf toten Connections

Wenn eine MorphiumDriverNetworkException die Connection schloss, hat der NetworkCallHelper auf derselben toten Connection retried — garantiert erfolglos. Jetzt wird vor jedem Retry isConnected() geprüft und bei Bedarf eine frische Connection aus dem Pool geholt.

IndexDescription.equals() False Mismatches

MongoDB gibt explizit false für boolean-Felder zurück, Java lässt sie null. Der Vergleich sah die als unterschiedlich → Indizes erschienen bei jedem Start als "fehlend" → wiederholte Create-Index-Versuche die mit "Index already exists" fehlschlugen.

Upgrade

<dependency>
    <groupId>de.caluga</groupId>
    <artifactId>morphium</artifactId>
    <version>6.2.2</version>
</dependency>

Keine Breaking Changes gegenüber 6.2.0. Das vollständige Changelog gibt's auf GitHub.