GH-8550: MQTT: Always re-subscribe on re-connect (#8553)

Fixes https://github.com/spring-projects/spring-integration/issues/8550

Turns out the Paho MQTT client does not re-subscribe when connection re-established on automatic reconnection

* Fix `Mqttv5PahoMessageDrivenChannelAdapter` to always subscribe to its topics in the `connectComplete()` independently of the `reconnect` status
* Verify behavior with `MOSQUITTO_CONTAINER` image restart in Docker
This commit is contained in:
Artem Bilan
2023-02-15 17:34:20 -05:00
committed by GitHub
parent b088091902
commit 8e27b1ce1b
2 changed files with 146 additions and 22 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2021-2022 the original author or authors.
* Copyright 2021-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -91,7 +91,7 @@ public class Mqttv5PahoMessageDrivenChannelAdapter extends AbstractMqttMessageDr
public Mqttv5PahoMessageDrivenChannelAdapter(String url, String clientId, String... topic) {
super(url, clientId, topic);
this.connectionOptions = new MqttConnectionOptions();
this.connectionOptions.setServerURIs(new String[]{ url });
this.connectionOptions.setServerURIs(new String[] {url});
this.connectionOptions.setAutomaticReconnect(true);
}
@@ -312,30 +312,28 @@ public class Mqttv5PahoMessageDrivenChannelAdapter extends AbstractMqttMessageDr
@Override
public void connectComplete(boolean reconnect, String serverURI) {
if (!reconnect) {
ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher();
String[] topics = getTopic();
this.topicLock.lock();
try {
if (topics.length > 0) {
int[] requestedQos = getQos();
this.mqttClient.subscribe(topics, requestedQos).waitForCompletion(getCompletionTimeout());
String message = "Connected and subscribed to " + Arrays.toString(topics);
logger.debug(message);
if (applicationEventPublisher != null) {
applicationEventPublisher.publishEvent(new MqttSubscribedEvent(this, message));
}
}
}
catch (MqttException ex) {
ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher();
String[] topics = getTopic();
this.topicLock.lock();
try {
if (topics.length > 0) {
int[] requestedQos = getQos();
this.mqttClient.subscribe(topics, requestedQos).waitForCompletion(getCompletionTimeout());
String message = "Connected and subscribed to " + Arrays.toString(topics);
logger.debug(message);
if (applicationEventPublisher != null) {
applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, ex));
applicationEventPublisher.publishEvent(new MqttSubscribedEvent(this, message));
}
logger.error(ex, () -> "Error subscribing to " + Arrays.toString(topics));
}
finally {
this.topicLock.unlock();
}
catch (MqttException ex) {
if (applicationEventPublisher != null) {
applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, ex));
}
logger.error(ex, () -> "Error subscribing to " + Arrays.toString(topics));
}
finally {
this.topicLock.unlock();
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.mqtt;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptionsBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.mqtt.event.MqttSubscribedEvent;
import org.springframework.integration.mqtt.inbound.Mqttv5PahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.outbound.Mqttv5PahoMessageHandler;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Artem Bilan
*
* @since 5.5.17
*/
@SpringJUnitConfig
@DirtiesContext
public class ResubscribeAfterAutomaticReconnectTests implements MosquittoContainerTest {
@Autowired
private MessageChannel mqttOutInput;
@Autowired
private PollableChannel fromMqttChannel;
@Autowired
private MqttConnectionOptions connectionOptions;
@Autowired
Config config;
@Test
void messageReceivedAfterResubscriptionOnLostConnection() throws InterruptedException {
GenericMessage<String> testMessage = new GenericMessage<>("test");
this.mqttOutInput.send(testMessage);
assertThat(this.fromMqttChannel.receive(10_000)).isNotNull();
MOSQUITTO_CONTAINER.stop();
MOSQUITTO_CONTAINER.start();
connectionOptions.setServerURIs(new String[] {MosquittoContainerTest.mqttUrl()});
assertThat(this.config.subscribeLatch.await(10, TimeUnit.SECONDS)).isTrue();
this.mqttOutInput.send(testMessage);
assertThat(this.fromMqttChannel.receive(10_000)).isNotNull();
}
@Configuration
@EnableIntegration
public static class Config {
CountDownLatch subscribeLatch = new CountDownLatch(2);
@Bean
public MqttConnectionOptions mqttConnectOptions() {
return new MqttConnectionOptionsBuilder()
.serverURI(MosquittoContainerTest.mqttUrl())
.automaticReconnect(true)
.build();
}
@Bean
@ServiceActivator(inputChannel = "mqttOutInput")
public Mqttv5PahoMessageHandler mqttOutHandler(MqttConnectionOptions mqttConnectOptions) {
Mqttv5PahoMessageHandler messageHandler =
new Mqttv5PahoMessageHandler(mqttConnectOptions, "mqttv5SIout");
messageHandler.setDefaultTopic("siTest");
return messageHandler;
}
@Bean
QueueChannel fromMqttChannel() {
return new QueueChannel();
}
@Bean
public Mqttv5PahoMessageDrivenChannelAdapter mqttInChannelAdapter(
MqttConnectionOptions mqttConnectOptions, QueueChannel fromMqttChannel) {
Mqttv5PahoMessageDrivenChannelAdapter messageProducer =
new Mqttv5PahoMessageDrivenChannelAdapter(mqttConnectOptions, "mqttInClient", "siTest");
messageProducer.setOutputChannel(fromMqttChannel);
return messageProducer;
}
@EventListener(MqttSubscribedEvent.class)
public void mqttEvents() {
this.subscribeLatch.countDown();
}
}
}