Skip to content

[mqtt.ruuvigateway] Fix flaky itest and NPE on thing disposal (test teardown) - #21360

Open
ssalonen wants to merge 2 commits into
openhab:mainfrom
ssalonen:claude/ruuvigateway-test-failure-7lb625
Open

[mqtt.ruuvigateway] Fix flaky itest and NPE on thing disposal (test teardown)#21360
ssalonen wants to merge 2 commits into
openhab:mainfrom
ssalonen:claude/ruuvigateway-test-failure-7lb625

Conversation

@ssalonen

@ssalonen ssalonen commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fix attempt for #21349

The RuuviGatewayTest#testHappyFlow itest fails intermittently on CI with

OFFLINE (BRIDGE_OFFLINE) ==> expected: <UNKNOWN> but was: <OFFLINE>

The tests assert the Ruuvi thing's status updates by absolute index, expecting
INITIALIZING, UNKNOWN, ONLINE("Waiting for initial data"). That sequence only
holds if the MQTT broker bridge is ONLINE when the Ruuvi thing is initialized:
AbstractMQTTThingHandler.initialize() calls bridgeStatusChanged(getBridgeStatus()),
which reports OFFLINE (BRIDGE_OFFLINE) straight away when the bridge is OFFLINE.
That extra update shifts every following index, and the bridge's default reconnect
delay (10 s, then 60 s) is longer than the waitForAssert budget, so the test can
never recover.

Two things in the test made it unable to cope:

  • createMqttBrokerBridge() only waited for the bridge handler to exist, never
    for the bridge to reach ONLINE, so Ruuvi things could be created against a bridge
    that was still connecting or had just dropped.
  • afterEach()'s cleanup was a no-op: things.stream().map(thing -> thingProvider.remove(...))
    has no terminal operation. Things were therefore disposed only later, when
    JavaOSGiTest unregisters the volatile storage service — after the Moquette broker
    had already been stopped — leaving bridge handlers whose connections keep
    reconnecting in the background and leak into the next test.

Changes:

  • Wait for the broker bridge to reach ONLINE before creating Ruuvi things.
  • Actually remove the things in afterEach, while the broker is still running, and
    wait for the handlers to be disposed.
  • Report the whole captured status update sequence when a status assertion fails,
    so this class of failure is diagnosable from the build log.
  • RuuviHandler.unsubscribeAll() used thenCompose(unsubscribeSuccessful -> null),
    which returns a null CompletionStage, so every disposal of a Ruuvi thing logged
    unsubscription on disposal failed ... NullPointerException from
    AbstractMQTTThingHandler.dispose(). Changed to thenAccept.

Developed with AI assistance.

claude added 2 commits August 11, 2026 04:00
The `testHappyFlow` itest failed upstream with

    OFFLINE (BRIDGE_OFFLINE) ==> expected: <UNKNOWN> but was: <OFFLINE>

The tests assert the thing status updates of the Ruuvi thing by their
index, and the sequence they expect (INITIALIZING, UNKNOWN, ONLINE) only
holds when the MQTT broker bridge is ONLINE while the Ruuvi thing is
initialized. `AbstractMQTTThingHandler.initialize()` immediately reports
OFFLINE (BRIDGE_OFFLINE) when the bridge is OFFLINE at that moment, which
shifts all the following indices and makes the test fail for good.

- Wait for the broker bridge to reach ONLINE before creating Ruuvi
  things, instead of only waiting for its handler to exist.
- Actually remove the things in `afterEach`. The previous
  `things.stream().map(...)` had no terminal operation, so nothing was
  removed and the handlers were disposed only later, when JavaOSGiTest
  unregisters the volatile storage service, i.e. after the MQTT broker
  had already been stopped. The broker connection of such a late disposed
  bridge handler keeps on reconnecting in the background and leaks into
  the next test. Things are now removed while the broker is still
  running, and the test waits for the handlers to be disposed.
- Report the whole captured status update sequence when a status
  assertion fails, so that this kind of failure can be diagnosed from the
  build log.

Signed-off-by: Sami Salonen <sami.salonen@mailhub.fi>
`thenCompose(unsubscribeSuccessful -> null)` returns a null
`CompletionStage`, so the returned future always completed
exceptionally with a NullPointerException. Every disposal of a Ruuvi
thing therefore logged

    unsubscription on disposal failed for mqtt:ruuvitag_beacon:...
    java.util.concurrent.ExecutionException: java.lang.NullPointerException

from `AbstractMQTTThingHandler.dispose()`. Use `thenAccept` instead,
which gives the `CompletableFuture<Void>` the method is supposed to
return.

Signed-off-by: Sami Salonen <sami.salonen@mailhub.fi>
@lsiepel lsiepel linked an issue Aug 11, 2026 that may be closed by this pull request
@lsiepel

lsiepel commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Not sure if you are aware, but the DCO is failing, could you add the sign-off message to the existing commits ?

@lsiepel lsiepel added the test label Aug 15, 2026
@lsiepel
lsiepel self-requested a review August 15, 2026 22:40

@lsiepel lsiepel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My codex friend found these two comments. See below. The requested test is usefull but optional. The catch block seems essential to get stable results.

@AfterEach
public void afterEach() throws Exception {
unregisterService(statusSubscriber);
removeThings();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If removeThings() times out or a removal throws, afterEach() exits here and never stops mqttConnection, shuts down the scheduler, or calls super.afterEach(). That leaves the very broker resources this change is intended to contain leaking into subsequent tests (and can mask the original failure). Please preserve the required thing-before-broker removal order, but protect the remaining cleanup with try/finally so it always runs.

String localTopic = topic;
if (localConnection != null && localTopic != null) {
return localConnection.unsubscribe(localTopic, this).thenCompose(unsubscribeSuccessful -> null);
return localConnection.unsubscribe(localTopic, this).thenAccept(unsubscribeSuccessful -> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a focused regression test for this completion-stage fix? The updated integration-test teardown only waits until thing.getHandler() is null. AbstractMQTTThingHandler.dispose() catches the ExecutionException produced by the old thenCompose(... -> null) implementation, after which the handler is still detached, so the old bug would satisfy the new assertion. A test should verify that unsubscribeAll() completes normally (or that the subscription is actually removed).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[mqtt.ruuvigateway] RuuviGatewayTest unstable

3 participants