feat: implement MQTT Last Will and Testament (LWT) - #6902
Conversation
Parse and store will fields (topic, message, QoS, retain) in WillRepository on CONNECT. Publish will message to subscribers on ungraceful disconnect via channelInactive hook. Clear will on graceful DISCONNECT. Fix DISCONNECT message dispatch in MqttFactory that was previously dropped. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Implements MQTT Last Will and Testament (LWT) support in the shenyu-protocol-mqtt module by persisting will data on CONNECT, clearing it on graceful DISCONNECT, and publishing it on ungraceful disconnect via Netty channelInactive.
Changes:
- Add
WillRepositoryto store per-connection LWT entries. - Publish stored wills on
channelInactiveand clear wills on DISCONNECT. - Add unit tests and module test dependencies/config to validate LWT behavior.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| shenyu-protocol/shenyu-protocol-mqtt/src/main/java/org/apache/shenyu/protocol/mqtt/Connect.java | Store will fields from CONNECT into WillRepository. |
| shenyu-protocol/shenyu-protocol-mqtt/src/main/java/org/apache/shenyu/protocol/mqtt/Disconnect.java | Clear will on graceful DISCONNECT before closing channel. |
| shenyu-protocol/shenyu-protocol-mqtt/src/main/java/org/apache/shenyu/protocol/mqtt/MqttTransportHandler.java | Publish + clear will on ungraceful disconnect (channelInactive). |
| shenyu-protocol/shenyu-protocol-mqtt/src/main/java/org/apache/shenyu/protocol/mqtt/Publish.java | Add publishWill(...) helper to emit will PUBLISH packets to subscribers. |
| shenyu-protocol/shenyu-protocol-mqtt/src/main/java/org/apache/shenyu/protocol/mqtt/MqttFactory.java | Ensure DISCONNECT is dispatched to messageType.disconnect(ctx). |
| shenyu-protocol/shenyu-protocol-mqtt/src/main/java/org/apache/shenyu/protocol/mqtt/repositories/WillRepository.java | New repository for storing will topic/message/QoS/retain by Channel. |
| shenyu-protocol/shenyu-protocol-mqtt/src/test/java/org/apache/shenyu/protocol/mqtt/ConnectTest.java | Tests will persistence on CONNECT. |
| shenyu-protocol/shenyu-protocol-mqtt/src/test/java/org/apache/shenyu/protocol/mqtt/DisconnectTest.java | Tests will clearance on DISCONNECT. |
| shenyu-protocol/shenyu-protocol-mqtt/src/test/java/org/apache/shenyu/protocol/mqtt/MqttTransportHandlerTest.java | Tests will firing/removal on channelInactive. |
| shenyu-protocol/shenyu-protocol-mqtt/src/test/java/org/apache/shenyu/protocol/mqtt/PublishWillTest.java | Tests will publishing behavior to subscribers. |
| shenyu-protocol/shenyu-protocol-mqtt/src/test/java/org/apache/shenyu/protocol/mqtt/repositories/WillRepositoryTest.java | Tests repository CRUD semantics for will entries. |
| shenyu-protocol/shenyu-protocol-mqtt/pom.xml | Add JUnit/Mockito deps and configure Surefire argLine for tests. |
Suppressed comments (1)
shenyu-protocol/shenyu-protocol-mqtt/src/main/java/org/apache/shenyu/protocol/mqtt/repositories/WillRepository.java:75
- WillEntry#getMessage currently returns the internal byte[] directly, allowing external callers to mutate repository state. Return a defensive copy instead.
public byte[] getMessage() {
return message;
}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // store will if present | ||
| if (msg.variableHeader().isWillFlag()) { | ||
| WillRepository.WillEntry will = new WillRepository.WillEntry( | ||
| msg.payload().willTopic(), | ||
| msg.payload().willMessageInBytes(), | ||
| msg.variableHeader().willQos(), | ||
| msg.variableHeader().isWillRetain()); | ||
| Singleton.INST.get(WillRepository.class).add(ctx.channel(), will); | ||
| } |
| @Override | ||
| public void channelInactive(final ChannelHandlerContext ctx) throws Exception { | ||
| WillRepository.WillEntry will = Singleton.INST.get(WillRepository.class).get(ctx.channel()); | ||
| if (Objects.nonNull(will)) { | ||
| Publish.publishWill(will); | ||
| Singleton.INST.get(WillRepository.class).remove(ctx.channel()); | ||
| } | ||
| super.channelInactive(ctx); | ||
| } |
| public WillEntry(final String topic, final byte[] message, final int qos, final boolean retain) { | ||
| this.topic = topic; | ||
| this.message = message; | ||
| this.qos = qos; | ||
| this.retain = retain; | ||
| } |
| @AfterEach | ||
| public void tearDown() { | ||
| channel.close(); | ||
| } |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Aias00
left a comment
There was a problem hiding this comment.
Review: #6902 — feat: implement MQTT Last Will and Testament (LWT)
Verdict: ✅ Approve (with one consistency follow-up)
Solid, well-tested feature implementation. The lifecycle handling is correct and the test coverage is excellent.
What's correct
- The
MqttFactoryDISCONNECT fix is the linchpin. Original code hadcase PUBACK: case DISCONNECT: default: break;— so DISCONNECT messages were silently dropped andDisconnect.disconnect()was never invoked. Without this fix, graceful-disconnect will removal couldn't work at all. Good catch, and it's required for the rest of the feature to function. - Correct will lifecycle:
- CONNECT with
isWillFlag()→WillRepository.add(channel, will)✅ - Graceful DISCONNECT →
WillRepository.remove(channel)→ will never fires ✅ - Ungraceful disconnect →
MqttTransportHandler.channelInactive()sees the will present →Publish.publishWill(will)→ removes it ✅ - The inactive channel is excluded from receiving its own will (
channel.isActive()guard) ✅ - No double-publish:
channelInactiveremoves the will immediately and Netty fires it once per close ✅
- CONNECT with
WillRepositoryis a cleanConcurrentHashMap<Channel, WillEntry>keyed by Channel (not clientId), so reconnects with a new channel don't collide, andtestReplaceWillEntryOnReconnectcovers the replace path.publishWillnull-guards topic/message, derives a valid packetId (0 for AT_MOST_ONCE, random otherwise), and respects the will QoS/retain flags.- Test coverage is genuinely thorough:
ConnectTest(store / no-will / qos0 / retain),DisconnectTest(clears will / no will / removes channel),MqttTransportHandlerTest(fires+removes / no will / post-disconnect),PublishWillTest(active / inactive-skip / empty / qos+retain), andWillRepositoryTest. The pom changes (junit-jupiter, mockito,--add-opensfor JDK 17) are the right scaffolding to support them.
Suggestion (non-blocking)
- Wildcard-aware will delivery.
Publish.publishWillusesSubscribeRepository.get(will.getTopic())— an exact lookup. A client subscribed to e.g.status/#will not receive a will published tostatus/client-001, even though normal publish routing should match it. Since #6906 (wildcard matching) addsgetChannelsByTopic, it would be consistent to route the will through that here too. Not blocking (LWT-to-wildcard-subscriber is an edge case), but worth aligning. - Retained will semantics. A will with
retain=trueis sent with the RETAIN flag, but there's no evidence the broker persists retained messages for later subscribers. That's a broader retained-message gap, outside this PR's scope — just flagging so it's tracked.
Verdict
Approving. The implementation is correct, the essential DISCONNECT routing bug is fixed, and the tests back the behavior end-to-end. Address the wildcard-delivery point as a small follow-up.
Publish.publishWill used an exact SubscribeRepository.get() lookup, so clients subscribed to wildcard filters (e.g. status/#) never received wills published to concrete topics like status/client-001. Port the TopicMatcher and SubscribeRepository.getChannelsByTopic from apache#6906 and route will delivery through it, consistent with normal publish routing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Thank you for the code review on this PR. Fix: Wildcard-aware will delivery — previously Publish.publishWill used SubscribeRepository.get(topic), an exact-match lookup, so a client subscribed to status/# never received a will published to status/client-001.
|
Parse and store will fields (topic, message, QoS, retain) in WillRepository on CONNECT. Publish will message to subscribers on ungraceful disconnect via channelInactive hook. Clear will on graceful DISCONNECT. Fix DISCONNECT message dispatch in MqttFactory that was previously dropped.
Make sure that:
./mvnw clean install -Dmaven.javadoc.skip=true.Summary
Core changes:
BaseRepository interface.
will via Publish.publishWill() and then clears it.
channel.
Tests (5 new test files):
close #6852