This is an automated email from the ASF dual-hosted git repository.
Croway pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.22.x by this push:
new 271e6a23c5e4 CAMEL-24511: camel-paho-mqtt5 - Restart route when
resubscribe fails after automatic reconnect
271e6a23c5e4 is described below
commit 271e6a23c5e47c32a1a458f7d0898d7767b08ec0
Author: JinyuChen97 <[email protected]>
AuthorDate: Thu Aug 27 16:22:58 2026 +0100
CAMEL-24511: camel-paho-mqtt5 - Restart route when resubscribe fails after
automatic reconnect
Backport of #25767 to camel-4.22.x.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
components/camel-paho-mqtt5/pom.xml | 5 +
.../component/paho/mqtt5/PahoMqtt5Consumer.java | 52 +++++-
.../mqtt5/PahoMqtt5ResubscribeFailureTest.java | 206 +++++++++++++++++++++
3 files changed, 258 insertions(+), 5 deletions(-)
diff --git a/components/camel-paho-mqtt5/pom.xml
b/components/camel-paho-mqtt5/pom.xml
index 1cbb8365baca..8c453ebe328f 100644
--- a/components/camel-paho-mqtt5/pom.xml
+++ b/components/camel-paho-mqtt5/pom.xml
@@ -52,6 +52,11 @@
<artifactId>camel-test-junit6</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
<!-- test infra -->
<dependency>
diff --git
a/components/camel-paho-mqtt5/src/main/java/org/apache/camel/component/paho/mqtt5/PahoMqtt5Consumer.java
b/components/camel-paho-mqtt5/src/main/java/org/apache/camel/component/paho/mqtt5/PahoMqtt5Consumer.java
index ef6413031742..deec07b82392 100644
---
a/components/camel-paho-mqtt5/src/main/java/org/apache/camel/component/paho/mqtt5/PahoMqtt5Consumer.java
+++
b/components/camel-paho-mqtt5/src/main/java/org/apache/camel/component/paho/mqtt5/PahoMqtt5Consumer.java
@@ -16,6 +16,9 @@
*/
package org.apache.camel.component.paho.mqtt5;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+
import org.apache.camel.AsyncCallback;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
@@ -41,6 +44,7 @@ public class PahoMqtt5Consumer extends DefaultConsumer {
private volatile String clientId;
private volatile boolean stopClient;
private volatile MqttConnectionOptions connectionOptions;
+ private final AtomicBoolean restarting = new AtomicBoolean(false);
public PahoMqtt5Consumer(Endpoint endpoint, Processor processor) {
super(endpoint, processor);
@@ -66,10 +70,7 @@ public class PahoMqtt5Consumer extends DefaultConsumer {
clientId = PahoMqtt5Endpoint.generateClientId();
}
stopClient = true;
- client = new MqttClient(
- getEndpoint().getConfiguration().getBrokerUrl(),
- clientId,
-
PahoMqtt5Endpoint.createMqttClientPersistence(getEndpoint().getConfiguration()));
+ client = createClient();
LOG.debug("Connecting client: {} to broker: {}", clientId,
getEndpoint().getConfiguration().getBrokerUrl());
if (getEndpoint().getConfiguration().isManualAcksEnabled()) {
client.setManualAcks(true);
@@ -86,7 +87,16 @@ public class PahoMqtt5Consumer extends DefaultConsumer {
try {
client.subscribe(getEndpoint().getTopic(),
getEndpoint().getConfiguration().getQos());
} catch (MqttException e) {
- LOG.error("MQTT resubscribe failed {}",
e.getMessage(), e);
+ if (stopClient) {
+ LOG.warn("MQTT resubscribe failed on reconnect,
restarting route for recovery: {}",
+ e.getMessage(), e);
+ restartRouteAsync();
+ } else {
+ LOG.error(
+ "MQTT resubscribe failed on reconnect with
externally provided client,"
+ + " route will not be auto-restarted:
{}",
+ e.getMessage(), e);
+ }
}
}
}
@@ -126,6 +136,31 @@ public class PahoMqtt5Consumer extends DefaultConsumer {
client.subscribe(getEndpoint().getTopic(),
getEndpoint().getConfiguration().getQos());
}
+ private void restartRouteAsync() {
+ if (!restarting.compareAndSet(false, true)) {
+ LOG.debug("Route restart already in progress, skipping duplicate
restart");
+ return;
+ }
+ String threadName = "PahoMqtt5-RestartRoute-" + getRouteId();
+ ExecutorService executor
+ =
getEndpoint().getCamelContext().getExecutorServiceManager().newSingleThreadExecutor(this,
threadName);
+ executor.submit(() -> {
+ try {
+ String routeId = getRouteId();
+ LOG.info("Stopping route {} for restart after resubscribe
failure", routeId);
+
getEndpoint().getCamelContext().getRouteController().stopRoute(routeId);
+ LOG.info("Restarting route {}", routeId);
+
getEndpoint().getCamelContext().getRouteController().startRoute(routeId);
+ } catch (Exception e) {
+ getExceptionHandler().handleException(
+ "Failed to restart route after resubscribe failure",
e);
+ } finally {
+ restarting.set(false);
+
getEndpoint().getCamelContext().getExecutorServiceManager().shutdownNow(executor);
+ }
+ });
+ }
+
@Override
protected void doStop() throws Exception {
super.doStop();
@@ -145,6 +180,13 @@ public class PahoMqtt5Consumer extends DefaultConsumer {
client = null;
}
+ MqttClient createClient() throws MqttException {
+ return new MqttClient(
+ getEndpoint().getConfiguration().getBrokerUrl(),
+ clientId,
+
PahoMqtt5Endpoint.createMqttClientPersistence(getEndpoint().getConfiguration()));
+ }
+
@Override
public PahoMqtt5Endpoint getEndpoint() {
return (PahoMqtt5Endpoint) super.getEndpoint();
diff --git
a/components/camel-paho-mqtt5/src/test/java/org/apache/camel/component/paho/mqtt5/PahoMqtt5ResubscribeFailureTest.java
b/components/camel-paho-mqtt5/src/test/java/org/apache/camel/component/paho/mqtt5/PahoMqtt5ResubscribeFailureTest.java
new file mode 100644
index 000000000000..5d952a9296a3
--- /dev/null
+++
b/components/camel-paho-mqtt5/src/test/java/org/apache/camel/component/paho/mqtt5/PahoMqtt5ResubscribeFailureTest.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.apache.camel.component.paho.mqtt5;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.Consumer;
+import org.apache.camel.Endpoint;
+import org.apache.camel.Processor;
+import org.apache.camel.ServiceStatus;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.eclipse.paho.mqttv5.client.MqttCallback;
+import org.eclipse.paho.mqttv5.client.MqttClient;
+import org.eclipse.paho.mqttv5.common.MqttException;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class PahoMqtt5ResubscribeFailureTest extends CamelTestSupport {
+
+ private static final String ROUTE_ID = "mqtt-consumer";
+
+ @Override
+ public boolean isUseAdviceWith() {
+ return true;
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ }
+ };
+ }
+
+ private MqttCallback startRouteWithExternalClient(MqttClient mockClient)
throws Exception {
+ PahoMqtt5Endpoint endpoint = context.getEndpoint(
+ "paho-mqtt5:test?brokerUrl=tcp://localhost:1883",
PahoMqtt5Endpoint.class);
+ endpoint.setClient(mockClient);
+
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("paho-mqtt5:test?brokerUrl=tcp://localhost:1883")
+ .id(ROUTE_ID)
+ .to("mock:result");
+ }
+ });
+
+ context.start();
+
+ return captureCallback(mockClient);
+ }
+
+ private MqttCallback startRouteWithOwnedClient(MqttClient mockClient)
throws Exception {
+ PahoMqtt5Configuration config = new PahoMqtt5Configuration();
+ config.setBrokerUrl("tcp://localhost:1883");
+
+ PahoMqtt5Component component = new PahoMqtt5Component(context) {
+ @Override
+ protected Endpoint createEndpoint(String uri, String remaining,
Map<String, Object> parameters) {
+ PahoMqtt5Endpoint endpoint = new PahoMqtt5Endpoint(uri,
remaining, this, config.copy()) {
+ @Override
+ public Consumer createConsumer(Processor processor) throws
Exception {
+ PahoMqtt5Consumer consumer = new
PahoMqtt5Consumer(this, processor) {
+ @Override
+ MqttClient createClient() {
+ return mockClient;
+ }
+ };
+ configureConsumer(consumer);
+ return consumer;
+ }
+ };
+ return endpoint;
+ }
+ };
+ context.addComponent("paho-mqtt5-owned", component);
+
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("paho-mqtt5-owned:test")
+ .id(ROUTE_ID)
+ .to("mock:result");
+ }
+ });
+
+ context.start();
+
+ return captureCallback(mockClient);
+ }
+
+ private MqttCallback captureCallback(MqttClient mockClient) throws
Exception {
+ ArgumentCaptor<MqttCallback> callbackCaptor =
ArgumentCaptor.forClass(MqttCallback.class);
+ verify(mockClient).setCallback(callbackCaptor.capture());
+ return callbackCaptor.getValue();
+ }
+
+ @Test
+ void resubscribeFailureWithExternalClientShouldNotRestartRoute() throws
Exception {
+ MqttClient mockClient = mock(MqttClient.class);
+ when(mockClient.isConnected()).thenReturn(true);
+
+ MqttCallback callback = startRouteWithExternalClient(mockClient);
+
+
assertThat(context.getRouteController().getRouteStatus(ROUTE_ID)).isEqualTo(ServiceStatus.Started);
+
+ doThrow(new MqttException(MqttException.REASON_CODE_CLIENT_EXCEPTION))
+ .when(mockClient).subscribe(anyString(), anyInt());
+
+ callback.connectComplete(true, "tcp://localhost:1883");
+
+ await().during(2, TimeUnit.SECONDS)
+ .atMost(3, TimeUnit.SECONDS)
+ .untilAsserted(() ->
assertThat(context.getRouteController().getRouteStatus(ROUTE_ID))
+ .isEqualTo(ServiceStatus.Started));
+ }
+
+ @Test
+ void resubscribeFailureWithOwnedClientShouldStopRoute() throws Exception {
+ MqttClient mockClient = mock(MqttClient.class);
+ when(mockClient.isConnected()).thenReturn(true);
+
+ MqttCallback callback = startRouteWithOwnedClient(mockClient);
+
+ doThrow(new MqttException(MqttException.REASON_CODE_CLIENT_EXCEPTION))
+ .when(mockClient).subscribe(anyString(), anyInt());
+
+ callback.connectComplete(true, "tcp://localhost:1883");
+
+ await().atMost(10, TimeUnit.SECONDS)
+ .untilAsserted(() ->
assertThat(context.getRouteController().getRouteStatus(ROUTE_ID))
+ .isEqualTo(ServiceStatus.Stopped));
+ }
+
+ @Test
+ void successfulResubscribeOnReconnectShouldKeepRouteStarted() throws
Exception {
+ MqttClient mockClient = mock(MqttClient.class);
+ when(mockClient.isConnected()).thenReturn(true);
+
+ MqttCallback callback = startRouteWithExternalClient(mockClient);
+
+ callback.connectComplete(true, "tcp://localhost:1883");
+
+ verify(mockClient, times(2)).subscribe("test", 2);
+
assertThat(context.getRouteController().getRouteStatus(ROUTE_ID)).isEqualTo(ServiceStatus.Started);
+ }
+
+ @Test
+ void duplicateReconnectsShouldNotCauseConcurrentRestarts() throws
Exception {
+ MqttClient mockClient = mock(MqttClient.class);
+ when(mockClient.isConnected()).thenReturn(true);
+
+ MqttCallback callback = startRouteWithOwnedClient(mockClient);
+
+ doThrow(new MqttException(MqttException.REASON_CODE_CLIENT_EXCEPTION))
+ .when(mockClient).subscribe(anyString(), anyInt());
+
+ callback.connectComplete(true, "tcp://localhost:1883");
+ callback.connectComplete(true, "tcp://localhost:1883");
+
+ await().atMost(10, TimeUnit.SECONDS)
+ .untilAsserted(() ->
assertThat(context.getRouteController().getRouteStatus(ROUTE_ID))
+ .isEqualTo(ServiceStatus.Stopped));
+ }
+
+ @Test
+ void initialConnectShouldNotResubscribe() throws Exception {
+ MqttClient mockClient = mock(MqttClient.class);
+ when(mockClient.isConnected()).thenReturn(true);
+
+ MqttCallback callback = startRouteWithExternalClient(mockClient);
+
+ callback.connectComplete(false, "tcp://localhost:1883");
+
+ verify(mockClient, times(1)).subscribe("test", 2);
+
assertThat(context.getRouteController().getRouteStatus(ROUTE_ID)).isEqualTo(ServiceStatus.Started);
+ }
+}