ARTEMIS-6189 QoS 1 & 2 message handling is not resilient - #6613
Conversation
The JournalHashMap collection ID was hardcoded as a primitive long. This generalizes it to a type parameter, enabling journal hash maps keyed by arbitrary types such as String. This is needed for MQTT packet-ID correlation, which is keyed by client ID. - Collection ID encoding/decoding is now delegated to the persister via abstract methods, rather than hardcoded as long read/write - The internal map storage switches from Netty's LongObjectHashMap to a standard HashMap to support non-primitive keys - Null-safety fix in JournalHashMap.remove() prevents an NPE when removing a key that doesn't exist Co-Authored-By: Claude <noreply@anthropic.com>
| * @return An array of integers representing the list of accepted QoS for each topic | ||
| */ | ||
| int[] addSubscriptions(List<MqttTopicSubscription> subscriptions, Integer subscriptionIdentifier) throws Exception { | ||
| session.awaitStart(); |
There was a problem hiding this comment.
This kind of thing won't scale very well... you will have a thread holding the start. If you have many threads starting you may starve and never be able to complete.
Isn't possible to fully play with callbacks?
There was a problem hiding this comment.
I've reworked this a bit exclusively within the MQTTSubscriptionManager. Overall it's more thread-safe now. There is technically a possibility of contention, but in practice I think it's fairly low.
4be3d39 to
d5baf0d
Compare
|
BTW, the full test-suite looks good on this. |
|
|
||
| private OperationContext sessionContext; | ||
|
|
||
| private volatile CountDownLatch startLatch; |
There was a problem hiding this comment.
this is dead now.. remove it
| } | ||
| MQTTSessionState removed = sessionStates.remove(clientId); | ||
| if (removed != null && removed.getSubscriptions().size() > 0) { | ||
| removeDurableSubscriptionState(clientId); |
There was a problem hiding this comment.
the method still exists.. where the state is being removed now?
We talked about the state is cleaned up when the session is removed.. but I don't see where that's happening.
There was a problem hiding this comment.
Removing the durable subscription state is now done in org.apache.activemq.artemis.core.protocol.mqtt.MQTTSubscriptionManager#clean which is always called in conjunction with org.apache.activemq.artemis.core.protocol.mqtt.MQTTStateManager#removeSessionState.
There was a problem hiding this comment.
I had the impression that removeDurableSubscriptionState is now dead code... make sure it's not
There was a problem hiding this comment.
It's used by org.apache.activemq.artemis.core.protocol.mqtt.MQTTSubscriptionManager#removeSubscriptions.
|
on the tests.. make sure you add tests for while paging... and along my review here.. make sure the cleanup state is okay... I couldn't find the right cleanup method now. but perhaps it's just I missed. |
Replace the in-memory and per-client resources with journal-persisted packet-ID correlation and DuplicateIDCache-backed packet ID caches so that QoS 1 and QoS 2 message flows survive broker restarts. - Packet ID assignments are persisted to the journal via a JournalHashMap so that when a message is redelivered after a broker restart the same packet ID is reused - Inbound QoS 2 duplicate detection and outbound QoS 2 PUBREC tracking use DuplicateIDCache instead of in-memory sets, surviving restarts - The per-client QoS 2 management queue (used for PUBREL persistence) is eliminated; PUBREL retransmission is driven by iterating the PUBREC cache on reconnect - Objects related to resilience (caches, journal maps) are only created when actually used, avoiding overhead for QoS 0/1-only clients - Sends and acknowledgements are now explicitly transactional, ensuring atomicity between message operations and correlation store and cache updates - Session startup is asynchronous with CountDownLatch synchronization, preventing a race where client packets arrive before the session is fully initialized after CONNACK - Duplicate QoS 2 publish detection is moved earlier into the protocol handler - MQTT message filters are constructed once in the protocol manager and shared, rather than duplicated per subscription manager instance - Each subscription item now directly holds its consumer reference, eliminating a separate consumer lookup map - Graceful handling of ActiveMQShutdownException during message deletion and rollback redelivery avoids misleading log messages during broker restart - Error logging throughout the MQTT protocol includes client ID and packet ID context for easier debugging Co-Authored-By: Claude <noreply@anthropic.com>
Add comprehensive tests for MQTT QoS 1 and QoS 2 resiliency across broker restarts and client reconnects. These tests use interceptors to interrupt message flows at every stage of the QoS handshake and verify correct redelivery, exactly-once semantics, and proper state cleanup. - QoS 1 subscriber tests cover broker restart and client disconnect both before and after PUBACK, verifying packet ID reuse with DUP flag - QoS 1 publisher tests are not needed because the broker has no intermediate state to maintain; it either persists the inbound message or it doesn't, and the client drives retransmission entirely on its own - QoS 2 publisher tests cover interruptions at all four stages of the handshake (before/after PUBREC, before/after PUBCOMP) for both broker restart and client disconnect - QoS 2 subscriber tests cover the same stages plus session expiry cleanup via zero-interval disconnect, timed scanner, and clean-start reconnect - A soak test runs many publishers and many subscribers sending many QoS 2 messages with periodic broker restarts, verifying exactly-once delivery - HiveMQ MQTT 5 client added as a test dependency Co-Authored-By: Claude <noreply@anthropic.com>
During repeated execution of MQTT tests I fixed these flaky tests: - Add a notification listener to the cluster remote subscribe test to synchronize on SESSION_CREATED, preventing a race in subscription takeover - Fix non-deterministic message ordering in testAutoDeleteRetainedQueue by consuming the retained message before publishing non-retained messages - Use Wait.assertFalse for async disconnect detection instead of an immediate assertion Co-Authored-By: Claude <noreply@anthropic.com>
This PR implements MQTT QoS 1 and QoS 2 resiliency so that in-flight message flows survive broker restarts/crashes and client reconnects.
See the Jira for details about exactly what isn't working.
See the individual commit messages for an overview of what's been changed.
I know this is big, but this was a complex task that required refactoring many core pieces of the MQTT implementation. It expanded a lot as I was working through it. Claude helped along the way.
The full test-suite is being run now, but I wanted to get it out for review sooner than later.