Conversation
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Aias00
left a comment
There was a problem hiding this comment.
Review: #6906 — feat: implement MQTT wildcard subscription matching
Verdict: ✅ Approve (with two follow-up suggestions, one a real correctness edge case)
This fills a genuinely broken feature — wildcard subscriptions silently never matched because Publish.send did an exact Map.get. The fix is well-structured.
What's correct
TopicMatcheris spec-accurate. I traced the algorithm across the test matrix and beyond:+matches exactly one level (sport/+/player1❌sport/tennis/stadium/player1, ✓sport/football/player1);#matches any number of levels incl. the parent (sport/#✓sport);#only matches when it's its own level (sport#/sport/tennis#correctly rejected);$-prefixed topics are not matched by a leading wildcard (#/+→ false) but are matched by explicit$SYS/#/$SYS/+— exactly MQTT-4.7.2-1;- null inputs return false (no NPE).
- The
get()exact-lookup method is retained and still used byadd()/remove()internally, so this isn't introducing dead code — good call keeping it. getChannelsByTopicis a clean O(N) scan that delegates entirely toTopicMatcher; no logic duplicated.TopicMatcherTestis thorough — exact, single-level, multi-level, mixed,$-topic, and null cases all covered.
Suggestions (non-blocking)
- Duplicate delivery on overlapping subscriptions (real, please track as a follow-up).
getChannelsByTopicdoesresult.addAll(entry.getValue())over every matching filter. If one client holds two overlapping subscriptions (e.g.sport/#and#), it appears under both keys, so a publish tosport/xadds the sameChanneltwice → the client receives the message twice. MQTT requires at most one delivery per publish per client. Collect into aSet<Channel>(orLinkedHashSetif you care about order/stability) before returning to avoid this. - Performance fast-path. Every publish now scans all subscriptions. For the very common case where the topic has an exact (non-wildcard) subscriber, you could
result.addAll(get(topic))first (O(1) exact hit) and then only scan filters containing+/#. Not necessary for correctness, just a scale consideration. - Minor: invalid filters (e.g.
#not as its own level, or trailing text after#) silently returnfalsehere. Optionally reject malformed filters at subscription time inSubscribe.addso bad subscriptions fail fast instead of silently never matching.
Verdict
Approving. The core matching logic is correct and well-tested, and get() is correctly preserved. Suggestion #1 (dedupe) is worth a quick follow-up PR before wildcard support sees production traffic with multi-subscription clients.
… filters Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Thank you for the code review on this PR. Fixes for the three review comments:
Tests added: TopicMatcherTest validation cases, new SubscribeRepositoryTest (dedup/fast-path), new SubscribeTest (rejection + SUBACK codes). |
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>
Aias00
left a comment
There was a problem hiding this comment.
Summary (re-review after update)
Re-reviewed after the branch was updated (prior review was dismissed by a rebase). The current version already addresses the duplicate-delivery concern I raised before: getChannelsByTopic now collects results into a LinkedHashSet, so a client with overlapping subscriptions (sport/# and #) receives the message at most once — exactly as MQTT requires.
What is correct:
- New
TopicMatcherimplementsmatches(filter, topic)andisValidFilter(filter)per MQTT-4.7, including the$-topic rule (leading+/#must not match$SYS-style topics) and the rule that wildcards occupy an entire level with#last. Subscribe.subscribefilters out invalid filters and rejects them in the SUBACK with QoSFAILURE(0x80) while accepting valid ones; only valid subscriptions are stored.SubscribeRepository.getChannelsByTopicscans stored filters, dedupes channels viaLinkedHashSet(overlapping wildcard filters deliver once per client), and uses the matcher for wildcard resolution.Publish.sendnow callsgetChannelsByTopicso publishes reach wildcard subscribers.- Tests are strong:
TopicMatcherTest(exact/single/multi/mixed wildcards,$topics, null, valid/invalid filters),SubscribeTest(invalid filter rejected with 0x80), andSubscribeRepositoryTest(exact/wildcard/dedup/multiple subscribers).
Conclusion
Approved. The matching logic is correct against the MQTT spec, the dedupe I previously flagged is now present, and the tests back the behavior.
Non-blocking suggestions
- Performance:
getChannelsByTopiciterates the entireTOPIC_CHANNEL_FACTORYmap on every publish (O(number of distinct subscription filters) per publish). For shenyu's expected scale this is acceptable; if subscription counts grow, an index of wildcard filters would help. Not a blocker. - Operator precedence nit:
filter.equals(topic) || filter.indexOf('+') < 0 && filter.indexOf('#') < 0relies on&&binding tighter than||. Correct today, but parentheses would make intent clearer and prevent future mis-edits. - Merge ordering: this PR and #6902 add the same
getChannelsByTopictoSubscribeRepository, while #6913 rewritesSubscribeRepositorytoMap<Channel, MqttQoS>. Merge #6902/#6906 first and rebase #6913 (or merge #6913 last) to avoid a conflict. Coordinate the three MQTT PRs.
Thanks for adding the dedupe.
…plemented # Conflicts: # shenyu-protocol/shenyu-protocol-mqtt/src/main/java/org/apache/shenyu/protocol/mqtt/repositories/SubscribeRepository.java # shenyu-protocol/shenyu-protocol-mqtt/src/test/java/org/apache/shenyu/protocol/mqtt/repositories/SubscribeRepositoryTest.java
- use EmbeddedChannel instead of a Mockito mock for the subscribers: the inline mock maker cannot self attach on the JDK used here, so both remove tests errored out - unify the async waits on Awaitility and drop the hand written sleep loop - release topics and subscribers around every test because SubscribeRepository keeps its subscriptions in a static map shared with the other test classes - cover multi level wildcard matching and a channel that unsubscribed
dengliming
left a comment
There was a problem hiding this comment.
This test looks flaky / incorrect because it calls new Subscribe().subscribe(ctx, msg) on a plain EmbeddedChannel, but Subscribe.subscribe(...) returns early when isConnected(channel) is false. In that case repository.add(...) is never reached, so awaitUntil(() -> repository.get("sport/#").contains(channel)) can only time out.
Could we update the test setup to put the channel into the same connected/authenticated state that MessageType.isConnected(...) expects before invoking subscribe(...)? Otherwise this test is asserting a post-condition that the method never has a chance to produce.
Aias00
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES (PMC Aias00): the main build job is failing — this PR does not compile/pass tests, so it cannot be merged. Please fix the compilation/test failures and re-run CI, then request a re-review.
Make sure that:
./mvnw clean install -Dmaven.javadoc.skip=true.Summary:
Problem
Subscriptions using + (single-level wildcard) or # (multi-level wildcard) were silently broken. Publish.send() performed an exact-key lookup (ConcurrentHashMap.getOrDefault), so a publish to sensor/room1/temperature would never match a subscription filter like sensor/+/temperature.
Changes
- + matches exactly one topic level
- # matches any number of levels (must appear at the end of the filter)
- Wildcards at the first level do not match $-prefixed topics
the published topic using TopicMatcher.matches().
close #6851