INT-3900: MQTT: Don't Unsubscribe if !cleanSession
JIRA: https://jira.spring.io/browse/INT-3900 The message-driven adapter should not unsubscribe if `cleanSession` is `false`. (cherry picked from commit 616fc79)
This commit is contained in:
committed by
Artem Bilan
parent
373bdfc617
commit
bcc499786c
@@ -161,7 +161,7 @@ public class FileTailingMessageProducerTests {
|
||||
foo.flush();
|
||||
foo.close();
|
||||
for (int i = 0; i < 50; i++) {
|
||||
Message<?> message = outputChannel.receive(5000);
|
||||
Message<?> message = outputChannel.receive(10000);
|
||||
assertNotNull("expected a non-null message", message);
|
||||
assertEquals("hello" + i, message.getPayload());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2015 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
|
||||
*
|
||||
* 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.springframework.integration.mqtt.core;
|
||||
|
||||
/**
|
||||
* Action to take regarding subscrptions when consumer stops.
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 4.2.3
|
||||
*
|
||||
*/
|
||||
public enum ConsumerStopAction {
|
||||
|
||||
/**
|
||||
* Never unsubscribe.
|
||||
*/
|
||||
UNSUBSCRIBE_NEVER,
|
||||
|
||||
/**
|
||||
* Always unsubscribe.
|
||||
*/
|
||||
UNSUBSCRIBE_ALWAYS,
|
||||
|
||||
/**
|
||||
* Unsubscribe if clean session is true.
|
||||
*/
|
||||
UNSUBSCRIBE_CLEAN
|
||||
|
||||
}
|
||||
@@ -58,6 +58,8 @@ public class DefaultMqttPahoClientFactory implements MqttPahoClientFactory {
|
||||
|
||||
private volatile String[] serverURIs;
|
||||
|
||||
private volatile ConsumerStopAction consumerStopAction = ConsumerStopAction.UNSUBSCRIBE_CLEAN;
|
||||
|
||||
public void setCleanSession(Boolean cleanSession) {
|
||||
this.cleanSession = cleanSession;
|
||||
}
|
||||
@@ -110,6 +112,28 @@ public class DefaultMqttPahoClientFactory implements MqttPahoClientFactory {
|
||||
this.serverURIs = Arrays.copyOf(serverURIs, serverURIs.length);
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: move to interface in 4.3.
|
||||
*/
|
||||
/**
|
||||
* Get the consumer stop action.
|
||||
* @return the consumer stop action.
|
||||
* @since 4.2.3
|
||||
*/
|
||||
public ConsumerStopAction getConsumerStopAction() {
|
||||
return consumerStopAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the consumer stop action. Determines whether we unsubscribe when the consumer stops.
|
||||
* Default: {@link ConsumerStopAction#UNSUBSCRIBE_CLEAN}.
|
||||
* @param consumerStopAction the consumer stop action.
|
||||
* @since 4.2.3.
|
||||
*/
|
||||
public void setConsumerStopAction(ConsumerStopAction consumerStopAction) {
|
||||
this.consumerStopAction = consumerStopAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MqttClient getClientInstance(String uri, String clientId) throws MqttException {
|
||||
// Client validates URI even if overridden by options
|
||||
|
||||
@@ -35,6 +35,7 @@ public interface MqttPahoClientFactory {
|
||||
* @return The client instance.
|
||||
* @throws MqttException Any.
|
||||
*/
|
||||
// TODO: change return type to IMqttClient in 4.3
|
||||
MqttClient getClientInstance(String url, String clientId) throws MqttException;
|
||||
|
||||
/**
|
||||
@@ -46,6 +47,7 @@ public interface MqttPahoClientFactory {
|
||||
* @throws MqttException Any.
|
||||
* @since 4.1
|
||||
*/
|
||||
// TODO: change return type to IMqttAsyncClient in 4.3
|
||||
MqttAsyncClient getAsyncClientInstance(String url, String clientId) throws MqttException;
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,8 +18,8 @@ package org.springframework.integration.mqtt.inbound;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
|
||||
import org.eclipse.paho.client.mqttv3.IMqttAsyncClient;
|
||||
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
|
||||
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
|
||||
import org.eclipse.paho.client.mqttv3.MqttCallback;
|
||||
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
|
||||
import org.eclipse.paho.client.mqttv3.MqttException;
|
||||
@@ -27,6 +27,7 @@ import org.eclipse.paho.client.mqttv3.MqttMessage;
|
||||
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.integration.mqtt.core.ConsumerStopAction;
|
||||
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
|
||||
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
|
||||
import org.springframework.integration.mqtt.event.MqttConnectionFailedEvent;
|
||||
@@ -51,7 +52,7 @@ public class MqttPahoMessageDrivenChannelAdapter extends AbstractMqttMessageDriv
|
||||
|
||||
private final MqttPahoClientFactory clientFactory;
|
||||
|
||||
private volatile MqttAsyncClient client;
|
||||
private volatile IMqttAsyncClient client;
|
||||
|
||||
private volatile ScheduledFuture<?> reconnectFuture;
|
||||
|
||||
@@ -61,6 +62,10 @@ public class MqttPahoMessageDrivenChannelAdapter extends AbstractMqttMessageDriv
|
||||
|
||||
private volatile int recoveryInterval = DEFAULT_RECOVERY_INTERVAL;
|
||||
|
||||
private volatile boolean cleanSession;
|
||||
|
||||
private volatile ConsumerStopAction consumerStopAction;
|
||||
|
||||
private ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
/**
|
||||
@@ -148,8 +153,12 @@ public class MqttPahoMessageDrivenChannelAdapter extends AbstractMqttMessageDriv
|
||||
super.doStop();
|
||||
if (this.client != null) {
|
||||
try {
|
||||
this.client.unsubscribe(getTopic())
|
||||
.waitForCompletion(this.completionTimeout);
|
||||
if (this.consumerStopAction.equals(ConsumerStopAction.UNSUBSCRIBE_ALWAYS)
|
||||
|| (this.consumerStopAction.equals(ConsumerStopAction.UNSUBSCRIBE_CLEAN)
|
||||
&& this.cleanSession)) {
|
||||
this.client.unsubscribe(getTopic())
|
||||
.waitForCompletion(this.completionTimeout);
|
||||
}
|
||||
}
|
||||
catch (MqttException e) {
|
||||
logger.error("Exception while unsubscribing", e);
|
||||
@@ -211,6 +220,10 @@ public class MqttPahoMessageDrivenChannelAdapter extends AbstractMqttMessageDriv
|
||||
|
||||
private void connectAndSubscribe() throws MqttException {
|
||||
MqttConnectOptions connectionOptions = this.clientFactory.getConnectionOptions();
|
||||
this.cleanSession = connectionOptions.isCleanSession();
|
||||
this.consumerStopAction = this.clientFactory instanceof DefaultMqttPahoClientFactory ?
|
||||
((DefaultMqttPahoClientFactory) this.clientFactory).getConsumerStopAction() :
|
||||
ConsumerStopAction.UNSUBSCRIBE_CLEAN;
|
||||
Assert.state(getUrl() != null || connectionOptions.getServerURIs() != null,
|
||||
"If no 'url' provided, connectionOptions.getServerURIs() must not be null");
|
||||
this.client = this.clientFactory.getAsyncClientInstance(getUrl(), getClientId());
|
||||
|
||||
@@ -26,6 +26,7 @@ import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -41,21 +42,28 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.net.SocketFactory;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.eclipse.paho.client.mqttv3.IMqttToken;
|
||||
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
|
||||
import org.eclipse.paho.client.mqttv3.MqttCallback;
|
||||
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
|
||||
import org.eclipse.paho.client.mqttv3.MqttDeliveryToken;
|
||||
import org.eclipse.paho.client.mqttv3.MqttException;
|
||||
import org.eclipse.paho.client.mqttv3.MqttMessage;
|
||||
import org.eclipse.paho.client.mqttv3.MqttSecurityException;
|
||||
import org.eclipse.paho.client.mqttv3.MqttToken;
|
||||
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
|
||||
import org.junit.Test;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import org.springframework.aop.framework.ProxyFactoryBean;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.integration.channel.NullChannel;
|
||||
import org.springframework.integration.channel.QueueChannel;
|
||||
import org.springframework.integration.mqtt.core.ConsumerStopAction;
|
||||
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
|
||||
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory.Will;
|
||||
import org.springframework.integration.mqtt.event.MqttConnectionFailedEvent;
|
||||
@@ -74,6 +82,22 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
*/
|
||||
public class MqttAdapterTests {
|
||||
|
||||
private IMqttToken alwaysComplete;
|
||||
|
||||
{
|
||||
ProxyFactoryBean pfb = new ProxyFactoryBean();
|
||||
pfb.addAdvice(new MethodInterceptor() {
|
||||
|
||||
@Override
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
return null;
|
||||
}
|
||||
|
||||
});
|
||||
pfb.setInterfaces(IMqttToken.class);
|
||||
this.alwaysComplete = (IMqttToken) pfb.getObject();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPahoConnectOptions() {
|
||||
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
|
||||
@@ -313,4 +337,88 @@ public class MqttAdapterTests {
|
||||
assertEquals("Connected and subscribed to [baz, fix]", ((MqttSubscribedEvent) event).getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStopActionDefault() throws Exception {
|
||||
final MqttAsyncClient client = mock(MqttAsyncClient.class);
|
||||
MqttPahoMessageDrivenChannelAdapter adapter = buildAdapter(client, null, null);
|
||||
|
||||
adapter.start();
|
||||
adapter.stop();
|
||||
verifyUnsubscribe(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStopActionDefaultNotClean() throws Exception {
|
||||
final MqttAsyncClient client = mock(MqttAsyncClient.class);
|
||||
MqttPahoMessageDrivenChannelAdapter adapter = buildAdapter(client, false, null);
|
||||
|
||||
adapter.start();
|
||||
adapter.stop();
|
||||
verifyNotUnsubscribe(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStopActionAlways() throws Exception {
|
||||
final MqttAsyncClient client = mock(MqttAsyncClient.class);
|
||||
MqttPahoMessageDrivenChannelAdapter adapter = buildAdapter(client, false,
|
||||
ConsumerStopAction.UNSUBSCRIBE_ALWAYS);
|
||||
|
||||
adapter.start();
|
||||
adapter.stop();
|
||||
verifyUnsubscribe(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStopActionNever() throws Exception {
|
||||
final MqttAsyncClient client = mock(MqttAsyncClient.class);
|
||||
MqttPahoMessageDrivenChannelAdapter adapter = buildAdapter(client, null, ConsumerStopAction.UNSUBSCRIBE_NEVER);
|
||||
|
||||
adapter.start();
|
||||
adapter.stop();
|
||||
verifyNotUnsubscribe(client);
|
||||
}
|
||||
|
||||
private MqttPahoMessageDrivenChannelAdapter buildAdapter(final MqttAsyncClient client, Boolean cleanSession,
|
||||
ConsumerStopAction action) throws MqttException, MqttSecurityException {
|
||||
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory() {
|
||||
|
||||
@Override
|
||||
public MqttAsyncClient getAsyncClientInstance(String uri, String clientId) throws MqttException {
|
||||
return client;
|
||||
}
|
||||
|
||||
};
|
||||
factory.setServerURIs("tcp://localhost:1883");
|
||||
if (cleanSession != null) {
|
||||
factory.setCleanSession(cleanSession);
|
||||
}
|
||||
if (action != null) {
|
||||
factory.setConsumerStopAction(action);
|
||||
}
|
||||
when(client.connect(any(MqttConnectOptions.class))).thenReturn(this.alwaysComplete);
|
||||
when(client.subscribe(any(String[].class), any(int[].class))).thenReturn(this.alwaysComplete);
|
||||
when(client.disconnect()).thenReturn(this.alwaysComplete);
|
||||
when(client.unsubscribe(any(String[].class))).thenReturn(this.alwaysComplete);
|
||||
when(client.isConnected()).thenReturn(true);
|
||||
MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter("client", factory, "foo");
|
||||
adapter.setApplicationEventPublisher(mock(ApplicationEventPublisher.class));
|
||||
adapter.setOutputChannel(new NullChannel());
|
||||
adapter.afterPropertiesSet();
|
||||
return adapter;
|
||||
}
|
||||
|
||||
private void verifyUnsubscribe(MqttAsyncClient client) throws Exception {
|
||||
verify(client).connect(any(MqttConnectOptions.class));
|
||||
verify(client).subscribe(any(String[].class), any(int[].class));
|
||||
verify(client).unsubscribe(any(String[].class));
|
||||
verify(client).disconnect();
|
||||
}
|
||||
|
||||
private void verifyNotUnsubscribe(MqttAsyncClient client) throws Exception {
|
||||
verify(client).connect(any(MqttConnectOptions.class));
|
||||
verify(client).subscribe(any(String[].class), any(int[].class));
|
||||
verify(client, never()).unsubscribe(any(String[].class));
|
||||
verify(client).disconnect();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ A minimal configuration might be:
|
||||
|
||||
Attributes:
|
||||
|
||||
[source]
|
||||
[source,xml]
|
||||
----
|
||||
<int-mqtt:message-driven-channel-adapter id="oneTopicAdapter"
|
||||
client-id="foo" <1>
|
||||
@@ -89,6 +89,22 @@ Also, a new property `recoveryInterval` controls the interval at which the adapt
|
||||
a failure; it defaults to `10000ms` (ten seconds).
|
||||
This is not currently available using XML configuration.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Prior to _version 4.2.3_, the client always unsubscribed when the adapter was stopped.
|
||||
This is incorrect because if the client QOS is > 1, we need to keep the subscription active so that messages arriving
|
||||
while the adapter is stopped will be delivered on the next start.
|
||||
This also requires setting the `cleanSession` property on the client factory to `false` - it defaults to `true`.
|
||||
|
||||
Starting with _version 4.2.3_, the adapter will not unsubscribe (by default) if the `cleanSession` property is `false`.
|
||||
|
||||
This behavior can be overridden by setting the `consumerCloseAction` property on the factory.
|
||||
It can have values: `UNSUBSCRIBE_ALWAYS`, `UNSUBSCRIBE_NEVER`, and `UNSUBSCRIBE_CLEAN`.
|
||||
The latter (the default) will unsubscribe only if the `cleanSession` property is `true`.
|
||||
|
||||
To revert to the pre-4.2.3 behavior, use `UNSUBSCRIBE_ALWAYS`.
|
||||
====
|
||||
|
||||
==== Adding/Removing Topics at Runtime
|
||||
|
||||
Starting with _version 4.1_, it is possible to programmatically change the topics to which the adapter is subscribed.
|
||||
@@ -159,7 +175,7 @@ Starting with _version 4.1_, the adapter supports asynchronous sends, avoiding b
|
||||
|
||||
Attributes:
|
||||
|
||||
[source]
|
||||
[source,xml]
|
||||
----
|
||||
<int-mqtt:outbound-channel-adapter id="withConverter"
|
||||
client-id="foo" <1>
|
||||
|
||||
Reference in New Issue
Block a user