/*
* Copyright 2016-2017 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.amqp.rabbit;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Address;
import org.springframework.amqp.core.AmqpMessageReturnedException;
import org.springframework.amqp.core.AmqpReplyTimeoutException;
import org.springframework.amqp.core.AnonymousQueue;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.AsyncRabbitTemplate.RabbitConverterFuture;
import org.springframework.amqp.rabbit.AsyncRabbitTemplate.RabbitMessageFuture;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.junit.BrokerRunning;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
import org.springframework.amqp.rabbit.listener.adapter.ReplyingMessageListener;
import org.springframework.amqp.support.converter.SimpleMessageConverter;
import org.springframework.amqp.utils.test.TestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
/**
* @author Gary Russell
* @since 1.6
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
public class AsyncRabbitTemplateTests {
@Rule
public BrokerRunning brokerRunning = BrokerRunning.isRunning();
@Autowired
private AsyncRabbitTemplate asyncTemplate;
@Autowired
private AsyncRabbitTemplate asyncDirectTemplate;
@Autowired
private Queue requests;
@Autowired
private AtomicReference<CountDownLatch> latch;
private final Message fooMessage = new SimpleMessageConverter().toMessage("foo", new MessageProperties());
@Test
public void testConvert1Arg() throws Exception {
final AtomicBoolean mppCalled = new AtomicBoolean();
ListenableFuture<String> future = this.asyncTemplate.convertSendAndReceive("foo", m -> {
mppCalled.set(true);
return m;
});
checkConverterResult(future, "FOO");
assertTrue(mppCalled.get());
}
@Test
public void testConvert1ArgDirect() throws Exception {
this.latch.set(new CountDownLatch(1));
ListenableFuture<String> future1 = this.asyncDirectTemplate.convertSendAndReceive("foo");
ListenableFuture<String> future2 = this.asyncDirectTemplate.convertSendAndReceive("bar");
this.latch.get().countDown();
checkConverterResult(future1, "FOO");
checkConverterResult(future2, "BAR");
this.latch.set(null);
waitForZeroInUseConsumers();
assertThat(TestUtils
.getPropertyValue(this.asyncDirectTemplate, "directReplyToContainer.consumerCount", Integer.class),
equalTo(2));
final String missingQueue = UUID.randomUUID().toString();
this.asyncDirectTemplate.convertSendAndReceive("", missingQueue, "foo"); // send to nowhere
this.asyncDirectTemplate.stop(); // should clear the inUse channel map
waitForZeroInUseConsumers();
this.asyncDirectTemplate.start();
this.asyncDirectTemplate.setReceiveTimeout(1);
this.asyncDirectTemplate.convertSendAndReceive("", missingQueue, "foo"); // send to nowhere
waitForZeroInUseConsumers();
this.asyncDirectTemplate.setReceiveTimeout(10000);
this.asyncDirectTemplate.convertSendAndReceive("", missingQueue, "foo").cancel(true);
waitForZeroInUseConsumers();
}
@Test
public void testConvert2Args() throws Exception {
ListenableFuture<String> future = this.asyncTemplate.convertSendAndReceive(this.requests.getName(), "foo");
checkConverterResult(future, "FOO");
}
@Test
public void testConvert3Args() throws Exception {
ListenableFuture<String> future = this.asyncTemplate.convertSendAndReceive("", this.requests.getName(), "foo");
checkConverterResult(future, "FOO");
}
@Test
public void testConvert4Args() throws Exception {
ListenableFuture<String> future = this.asyncTemplate.convertSendAndReceive("", this.requests.getName(), "foo",
message -> {
String body = new String(message.getBody());
return new Message((body + "bar").getBytes(), message.getMessageProperties());
});
checkConverterResult(future, "FOOBAR");
}
@Test
public void testMessage1Arg() throws Exception {
ListenableFuture<Message> future = this.asyncTemplate.sendAndReceive(getFooMessage());
checkMessageResult(future, "FOO");
}
@Test
public void testMessage1ArgDirect() throws Exception {
this.latch.set(new CountDownLatch(1));
ListenableFuture<Message> future1 = this.asyncDirectTemplate.sendAndReceive(getFooMessage());
ListenableFuture<Message> future2 = this.asyncDirectTemplate.sendAndReceive(getFooMessage());
this.latch.get().countDown();
Message reply1 = checkMessageResult(future1, "FOO");
assertEquals(Address.AMQ_RABBITMQ_REPLY_TO, reply1.getMessageProperties().getConsumerQueue());
Message reply2 = checkMessageResult(future2, "FOO");
assertEquals(Address.AMQ_RABBITMQ_REPLY_TO, reply2.getMessageProperties().getConsumerQueue());
this.latch.set(null);
waitForZeroInUseConsumers();
assertThat(TestUtils
.getPropertyValue(this.asyncDirectTemplate, "directReplyToContainer.consumerCount", Integer.class),
equalTo(2));
this.asyncDirectTemplate.stop();
this.asyncDirectTemplate.start();
assertThat(TestUtils
.getPropertyValue(this.asyncDirectTemplate, "directReplyToContainer.consumerCount", Integer.class),
equalTo(0));
}
private void waitForZeroInUseConsumers() throws InterruptedException {
int n = 0;
Map<?, ?> inUseConsumers = TestUtils
.getPropertyValue(this.asyncDirectTemplate, "directReplyToContainer.inUseConsumerChannels", Map.class);
while (n++ < 100 && inUseConsumers.size() > 0) {
Thread.sleep(100);
}
assertThat(inUseConsumers.size(), equalTo(0));
}
@Test
public void testMessage2Args() throws Exception {
ListenableFuture<Message> future = this.asyncTemplate.sendAndReceive(this.requests.getName(), getFooMessage());
checkMessageResult(future, "FOO");
}
@Test
public void testMessage3Args() throws Exception {
ListenableFuture<Message> future = this.asyncTemplate.sendAndReceive("", this.requests.getName(),
getFooMessage());
checkMessageResult(future, "FOO");
}
@Test
public void testCancel() throws Exception {
ListenableFuture<String> future = this.asyncTemplate.convertSendAndReceive("foo");
future.cancel(false);
assertEquals(0, TestUtils.getPropertyValue(asyncTemplate, "pending", Map.class).size());
}
@Test
public void testMessageCustomCorrelation() throws Exception {
Message message = getFooMessage();
message.getMessageProperties().setCorrelationId("foo");
ListenableFuture<Message> future = this.asyncTemplate.sendAndReceive(message);
Message result = checkMessageResult(future, "FOO");
assertEquals("foo", result.getMessageProperties().getCorrelationId());
}
private Message getFooMessage() {
this.fooMessage.getMessageProperties().setCorrelationId(null);
this.fooMessage.getMessageProperties().setReplyTo(null);
return this.fooMessage;
}
@Test
@DirtiesContext
public void testReturn() throws Exception {
this.asyncTemplate.setMandatory(true);
ListenableFuture<String> future = this.asyncTemplate.convertSendAndReceive(this.requests.getName() + "x", "foo");
try {
future.get(10, TimeUnit.SECONDS);
fail("Expected exception");
}
catch (ExecutionException e) {
assertThat(e.getCause(), instanceOf(AmqpMessageReturnedException.class));
assertEquals(this.requests.getName() + "x", ((AmqpMessageReturnedException) e.getCause()).getRoutingKey());
}
}
@Test
@DirtiesContext
public void testReturnDirect() throws Exception {
this.asyncDirectTemplate.setMandatory(true);
ListenableFuture<String> future = this.asyncDirectTemplate.convertSendAndReceive(this.requests.getName() + "x",
"foo");
try {
future.get(10, TimeUnit.SECONDS);
fail("Expected exception");
}
catch (ExecutionException e) {
assertThat(e.getCause(), instanceOf(AmqpMessageReturnedException.class));
assertEquals(this.requests.getName() + "x", ((AmqpMessageReturnedException) e.getCause()).getRoutingKey());
}
}
@Test
@DirtiesContext
public void testConvertWithConfirm() throws Exception {
this.asyncTemplate.setEnableConfirms(true);
RabbitConverterFuture<String> future = this.asyncTemplate.convertSendAndReceive("sleep");
ListenableFuture<Boolean> confirm = future.getConfirm();
assertNotNull(confirm);
assertTrue(confirm.get(10, TimeUnit.SECONDS));
checkConverterResult(future, "SLEEP");
}
@Test
@DirtiesContext
public void testMessageWithConfirm() throws Exception {
this.asyncTemplate.setEnableConfirms(true);
RabbitMessageFuture future = this.asyncTemplate
.sendAndReceive(new SimpleMessageConverter().toMessage("sleep", new MessageProperties()));
ListenableFuture<Boolean> confirm = future.getConfirm();
assertNotNull(confirm);
assertTrue(confirm.get(10, TimeUnit.SECONDS));
checkMessageResult(future, "SLEEP");
}
@Test
@DirtiesContext
public void testConvertWithConfirmDirect() throws Exception {
this.asyncDirectTemplate.setEnableConfirms(true);
RabbitConverterFuture<String> future = this.asyncDirectTemplate.convertSendAndReceive("sleep");
ListenableFuture<Boolean> confirm = future.getConfirm();
assertNotNull(confirm);
assertTrue(confirm.get(10, TimeUnit.SECONDS));
checkConverterResult(future, "SLEEP");
}
@Test
@DirtiesContext
public void testMessageWithConfirmDirect() throws Exception {
this.asyncDirectTemplate.setEnableConfirms(true);
RabbitMessageFuture future = this.asyncDirectTemplate
.sendAndReceive(new SimpleMessageConverter().toMessage("sleep", new MessageProperties()));
ListenableFuture<Boolean> confirm = future.getConfirm();
assertNotNull(confirm);
assertTrue(confirm.get(10, TimeUnit.SECONDS));
checkMessageResult(future, "SLEEP");
}
@Test
@DirtiesContext
public void testReceiveTimeout() throws Exception {
this.asyncTemplate.setReceiveTimeout(500);
ListenableFuture<String> future = this.asyncTemplate.convertSendAndReceive("noReply");
TheCallback callback = new TheCallback();
future.addCallback(callback);
assertEquals(1, TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class).size());
try {
future.get(10, TimeUnit.SECONDS);
fail("Expected ExecutionException");
}
catch (ExecutionException e) {
assertThat(e.getCause(), instanceOf(AmqpReplyTimeoutException.class));
}
assertEquals(0, TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class).size());
assertTrue(callback.latch.await(10, TimeUnit.SECONDS));
assertThat(callback.ex, instanceOf(AmqpReplyTimeoutException.class));
}
@Test
@DirtiesContext
public void testReplyAfterReceiveTimeout() throws Exception {
this.asyncTemplate.setReceiveTimeout(100);
RabbitConverterFuture<String> future = this.asyncTemplate.convertSendAndReceive("sleep");
TheCallback callback = new TheCallback();
future.addCallback(callback);
assertEquals(1, TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class).size());
try {
future.get(10, TimeUnit.SECONDS);
fail("Expected ExecutionException");
}
catch (ExecutionException e) {
assertThat(e.getCause(), instanceOf(AmqpReplyTimeoutException.class));
}
assertEquals(0, TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class).size());
assertTrue(callback.latch.await(10, TimeUnit.SECONDS));
assertThat(callback.ex, instanceOf(AmqpReplyTimeoutException.class));
/*
* Test there's no harm if the reply is received after the timeout. This
* is unlikely to happen because the future is removed from the pending
* map when it times out. However, there is a small race condition where
* the reply arrives at the same time as the timeout.
*/
future.set("foo");
assertNull(callback.result);
}
@Test
@DirtiesContext
public void testStopCancelled() throws Exception {
this.asyncTemplate.setReceiveTimeout(5000);
RabbitConverterFuture<String> future = this.asyncTemplate.convertSendAndReceive("noReply");
TheCallback callback = new TheCallback();
future.addCallback(callback);
assertEquals(1, TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class).size());
this.asyncTemplate.stop();
try {
future.get(10, TimeUnit.SECONDS);
fail("Expected CancellationException");
}
catch (CancellationException e) {
assertEquals("AsyncRabbitTemplate was stopped while waiting for reply", future.getNackCause());
}
assertEquals(0, TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class).size());
assertTrue(callback.latch.await(10, TimeUnit.SECONDS));
assertTrue(future.isCancelled());
assertNull(TestUtils.getPropertyValue(this.asyncTemplate, "taskScheduler"));
/*
* Test there's no harm if the reply is received after the cancel. This
* should never happen because the container is stopped before canceling
* and the future is removed from the pending map.
*/
future.set("foo");
assertNull(callback.result);
}
private void checkConverterResult(ListenableFuture<String> future, String expected) throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
final AtomicReference<String> resultRef = new AtomicReference<String>();
future.addCallback(new ListenableFutureCallback<String>() {
@Override
public void onSuccess(String result) {
resultRef.set(result);
latch.countDown();
}
@Override
public void onFailure(Throwable ex) {
latch.countDown();
}
});
assertTrue(latch.await(10, TimeUnit.SECONDS));
assertEquals(expected, resultRef.get());
}
private Message checkMessageResult(ListenableFuture<Message> future, String expected) throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
final AtomicReference<Message> resultRef = new AtomicReference<Message>();
future.addCallback(new ListenableFutureCallback<Message>() {
@Override
public void onSuccess(Message result) {
resultRef.set(result);
latch.countDown();
}
@Override
public void onFailure(Throwable ex) {
latch.countDown();
}
});
assertTrue(latch.await(10, TimeUnit.SECONDS));
assertEquals(expected, new String(resultRef.get().getBody()));
return resultRef.get();
}
public static class TheCallback implements ListenableFutureCallback<String> {
private final CountDownLatch latch = new CountDownLatch(1);
private volatile String result;
private volatile Throwable ex;
@Override
public void onSuccess(String result) {
this.result = result;
latch.countDown();
}
@Override
public void onFailure(Throwable ex) {
this.ex = ex;
latch.countDown();
}
}
@Configuration
public static class Config {
@Bean
public AtomicReference<CountDownLatch> latch() {
return new AtomicReference<>();
}
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory("localhost");
connectionFactory.setPublisherConfirms(true);
connectionFactory.setPublisherReturns(true);
return connectionFactory;
}
@Bean
public Queue requests() {
return new AnonymousQueue();
}
@Bean
public Queue replies() {
return new AnonymousQueue();
}
@Bean
public RabbitAdmin admin(ConnectionFactory connectionFactory) {
return new RabbitAdmin(connectionFactory);
}
@Bean
public RabbitTemplate template(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setRoutingKey(requests().getName());
return rabbitTemplate;
}
@Bean
public RabbitTemplate templateForDirect(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setRoutingKey(requests().getName());
return rabbitTemplate;
}
@Bean
@Primary
public SimpleMessageListenerContainer replyContainer(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
container.setQueueNames(replies().getName());
return container;
}
@Bean
public AsyncRabbitTemplate asyncTemplate(RabbitTemplate template, SimpleMessageListenerContainer container) {
return new AsyncRabbitTemplate(template, container);
}
@Bean
public AsyncRabbitTemplate asyncDirectTemplate(RabbitTemplate templateForDirect) {
return new AsyncRabbitTemplate(templateForDirect);
}
@Bean
public SimpleMessageListenerContainer remoteContainer(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
container.setQueueNames(requests().getName());
container.setMessageListener(
new MessageListenerAdapter((ReplyingMessageListener<String, String>)
message -> {
CountDownLatch countDownLatch = latch().get();
if (countDownLatch != null) {
try {
countDownLatch.await(10, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
if ("sleep".equals(message)) {
try {
Thread.sleep(500); // time for confirm to be delivered, or timeout to occur
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
else if ("noReply".equals(message)) {
return null;
}
return message.toUpperCase();
}));
return container;
}
}
}