Skip to content

fix(mqtt): scope session state per instance and guard a missing client id - #8092

Merged
yuluo-yx merged 1 commit into
apache:masterfrom
cloudbase-ai:fix/mqtt-session-scope-null-client-id
Oct 24, 2026
Merged

yuluo-yx merged 1 commit into
apache:masterfrom
cloudbase-ai:fix/mqtt-session-scope-null-client-id

Conversation

@cloudbase-ai

Copy link
Copy Markdown
Contributor

What this changes

SessionRepository was the only repository in shenyu-protocol-mqtt that kept its state in a
static map. It is now a per-instance map, like ChannelRepository, InflightRepository,
SubscribeRepository and WillRepository already are.

Subscribe and Unsubscribe looked the session up with the client id read from
ChannelRepository:

String clientId = Singleton.INST.get(ChannelRepository.class).get(ctx.channel());
MqttSession session = Singleton.INST.get(SessionRepository.class).get(clientId);

ChannelRepository.get(channel) returns null for a channel that CONNECT never registered, or
whose client id ownership was taken over by a newer connection (#7918). The session index is a
ConcurrentHashMap keyed by client id, and a null key throws:

java.lang.NullPointerException: Cannot invoke "Object.hashCode()" because "key" is null

so the whole SUBSCRIBE / UNSUBSCRIBE aborted before the SUBACK / UNSUBACK was written.
Publish.registerInbound and PubRel.pubRel already guard this case; the two handlers now do the
same and simply skip the session bookkeeping.

Why the tests passed before

Because the map was static, Singleton.INST.single(SessionRepository.class, new SessionRepository())
did not create fresh state: every instance shared one map, so a test could see sessions registered by
a test class that ran earlier in the same JVM. That masked both the missing fixtures and the null-key
NPE. MqttTransportHandlerTest shows the pattern clearly — run on its own, before this change, it
fails with Singleton.get(InflightRepository.class) returning null; it only passed as part of the
whole module because an earlier test class had left that registration behind.

Tests

Test What it pins
SessionRepositoryTest (new) two repositories do not share sessions, and remove only affects the owning instance
SubscribeQosTest#subscribeWithoutClientIdStillAcknowledges SUBACK is still written when the channel has no client id
UnsubscribeTest#unsubscribeWithoutClientIdStillAcknowledges UNSUBACK is still written when the channel has no client id
MqttTransportHandlerTest registers InflightRepository and TopicRepository, so the class no longer depends on leaked Singleton state

Reverting only the three main-code files makes all four new assertions fail with the exact
production error (Cannot invoke "Object.hashCode()" because "key" is null), which is the
regression this PR removes.

mvn -pl shenyu-protocol/shenyu-protocol-mqtt test
Tests run: 150, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

checkstyle:check on the module is clean.

Notes

  • No behaviour change on the happy path: a connected client always has a client id in
    ChannelRepository, so the session is still updated on SUBSCRIBE / UNSUBSCRIBE.
  • #8089 also targets the MQTT fixture isolation but does not compile (variable channelRepository is already defined in class); this PR supersedes it.

…t id (apache#8021)

SessionRepository kept its sessions in a static map while every other
repository in the package keeps them per instance. Two consequences:

- state leaked between instances, so a repository created for one
  connection, or for one test, could observe and evict the sessions of
  another;
- the module tests only passed because Singleton still held whatever the
  previously executed test class had registered.

Subscribe and Unsubscribe look the session up with the client id taken from
ChannelRepository, which is null for a channel that CONNECT never registered
or whose ownership was taken over. The session index rejects a null key, so
the SUBSCRIBE failed with an NPE before the SUBACK was sent. Publish and
PubRel already guard this case; the two handlers now do the same.

Tests: SessionRepositoryTest covers the instance isolation, SubscribeQosTest
and UnsubscribeTest cover the missing client id, and MqttTransportHandlerTest
registers the repositories it reaches through the handler so it no longer
depends on a leaked Singleton.
@yuluo-yx
yuluo-yx merged commit a8080ae into apache:master Oct 24, 2026
31 checks passed
This was referenced Oct 25, 2026
@Aias00 Aias00 added this to the 2.7.2 milestone Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants