caluga - java blog

Morphium 6.2.2 โ€” Bugfix Release with PoppyDB Hardening and Performance

Morphium 6.2.2 โ€” Bugfix Release with PoppyDB Hardening and Performance

Just two weeks after 6.2.0, Morphium 6.2.2 is here. No big feature release this time โ€” instead, a focused bugfix sprint that makes PoppyDB significantly more stable and faster. Plus a few things that have been bugging me for a while.

PoppyDB: From "mostly works" to "works"

PoppyDB got its own name and Maven module in 6.2.0. In 6.2.2, it gets the reliability it deserves. Almost 20 fixes target the server โ€” here are the most important ones:

Wire Protocol Corruption Fixed

This was the nastiest bug: When a change stream event arrived while the Netty I/O thread was writing another response on the same connection, bytes got interleaved. The result: Illegal opcode 0, corrupted messages, and clients that couldn't recover.

Root cause: CompletableFuture.whenComplete() wrote directly to the Netty channel from a background thread. Fix: Responses are now dispatched back to the Netty event loop thread via ctx.channel().eventLoop().execute(), serializing all writes per connection. This is how Netty is supposed to be used.

The same issue existed client-side: SingleMongoConnection.sendQuery() wasn't synchronized โ€” with shared connections from multiple threads, bytes got interleaved on the wire. sendQuery, sendCommand, and sendAndWaitForReply are now synchronized.

Thread Leak: 20,000 Threads Per Node

When clients disconnected without sending killCursors (which is what every MongoDB driver does), watch cursors and change stream subscriptions stayed alive forever. Each orphaned cursor blocked a thread in the executor pool and accumulated virtual threads for event dispatch. Under test load: over 20,000 threads per PoppyDB node, OOM.

Fix: Cursor IDs are now tracked per Netty channel and automatically killed in channelInactive().

Raft Election Stability

Three PoppyDB nodes on the same host with equal priority caused endless split-vote elections. Additionally, onLeaderDiscovered fired on every heartbeat instead of only on leader changes, and isLeader()/getCurrentLeader() in getHelloResult() weren't atomic โ€” the PooledDriver saw primary flapping events multiple times per second.

Fix: Generation guard for election timers, atomic leader snapshots, and nodes should use different priorities.

More PoppyDB Fixes

  • Update responses: returned "matched" instead of the MongoDB-standard "n" key โ†’ all updates failed
  • Find cursor batching: returned all documents in one response instead of batched โ†’ iterators broken
  • Upsert count: n: 0 instead of n: 1 for upserts โ†’ storeMap() assertions failed
  • writeErrors not forwarded: Duplicate key errors on upsert were silently dropped
  • Hostname 0.0.0.0: Hello response used bind address instead of hostname โ†’ clients couldn't connect
  • Tailable cursors: Direct insert path didn't call notifyTailableCursors

Performance: 4.7x Faster Startup

ClassGraphCache

Every new Morphium() used to trigger 2-4 ClassGraph classpath scans โ€” 100-500ms each. For tests with many Morphium instances, this dominated startup time. There's now a JVM-wide singleton cache: the first scan is cached, all subsequent instances reuse the results. In tests: BasicFunctionalityTest dropped from 67s to 14s.

More Performance Improvements

  • Zero-copy BSON decoder โ€” fewer allocations per document
  • Shallow copy instead of deep copy for change stream events
  • Direct dispatch for hot-path commands (insert, update, delete, find, count)
  • PoppyDB 3x faster than MongoDB for individual operations in local benchmarks (insert 0.74ms vs 4.48ms)

Important Core Bugfixes

Change Stream Events Lost After Collection Drop

Multiple race conditions in the InMemoryDriver could cause events to be lost or duplicated after a drop(). Fix: Sequence counter is advanced by 100 on drop, stale events are filtered, and history is purged before and after the drop notification.

Network Retry on Dead Connections

When a MorphiumDriverNetworkException closed the connection, NetworkCallHelper retried on the same dead connection โ€” guaranteed to fail again. Now checks isConnected() before each retry and gets a fresh connection from the pool if needed.

IndexDescription.equals() False Mismatches

MongoDB returns explicit false for boolean fields, Java leaves them null. The comparison saw them as different โ†’ indices appeared "missing" on every startup โ†’ repeated create-index attempts that failed with "Index already exists".

Upgrade

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

No breaking changes from 6.2.0. The full changelog is on GitHub.