Skip to content

Commit

Permalink
AMQP-832: Async @RabbitListener Return Types
Browse files Browse the repository at this point in the history
JIRA: https://jira.spring.io/browse/AMQP-832

Polishing - PR Comments - reactor optional

* Polishing imports for Checkstyle rules
* Use `ClassUtils.isPresent()` instead
* Fix the sentence to be present only as a single line
  • Loading branch information
garyrussell authored and artembilan committed Sep 7, 2018
1 parent b19ab35 commit e9d553a
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 10 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ subprojects { subproject ->
mockitoVersion = '2.18.0'
rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.4.1'
rabbitmqHttpClientVersion = '2.1.0.RELEASE'
reactorVersion = '3.1.6.RELEASE'

springVersion = project.hasProperty('springVersion') ? project.springVersion : '5.1.0.RC2'

Expand Down Expand Up @@ -264,6 +265,7 @@ project('spring-rabbit') {
compile "org.springframework:spring-context:$springVersion"
compile "org.springframework:spring-messaging:$springVersion"
compile "org.springframework:spring-tx:$springVersion"
compile ("io.projectreactor:reactor-core:$reactorVersion", optional)

compile ("ch.qos.logback:logback-classic:$logbackVersion", optional)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.function.Consumer;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
Expand Down Expand Up @@ -48,8 +49,11 @@
import org.springframework.retry.RecoveryCallback;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.concurrent.ListenableFuture;

import com.rabbitmq.client.Channel;
import reactor.core.publisher.Mono;

/**
* An abstract {@link MessageListener} adapter providing the necessary infrastructure
Expand All @@ -73,6 +77,9 @@ public abstract class AbstractAdaptableMessageListener implements ChannelAwareMe

private static final ParserContext PARSER_CONTEXT = new TemplateParserContext("!{", "}");

private static final boolean monoPresent =
ClassUtils.isPresent("reactor.core.publisher.Mono", ChannelAwareMessageListener.class.getClassLoader());;

/** Logger available to subclasses. */
protected final Log logger = LogFactory.getLog(getClass());

Expand Down Expand Up @@ -301,18 +308,18 @@ protected void handleResult(InvocationResult resultArg, Message request, Channel
*/
protected void handleResult(InvocationResult resultArg, Message request, Channel channel, Object source) {
if (channel != null) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Listener method returned result [" + resultArg
+ "] - generating response message for it");
if (resultArg.getReturnValue() instanceof ListenableFuture) {
((ListenableFuture<?>) resultArg.getReturnValue()).addCallback(
r -> asyncSuccess(resultArg, request, channel, source, r),
t -> asyncFailure(request, channel, t));
}
try {
Message response = buildMessage(channel, resultArg.getReturnValue(), resultArg.getReturnType());
postProcessResponse(request, response);
Address replyTo = getReplyToAddress(request, source, resultArg);
sendResponse(channel, replyTo, response);
else if (monoPresent && MonoHandler.isMono(resultArg.getReturnValue())) {
MonoHandler.subscribe(resultArg.getReturnValue(),
r -> asyncSuccess(resultArg, request, channel, source, r),
t -> asyncFailure(request, channel, t));
}
catch (Exception ex) {
throw new ReplyFailureException("Failed to send reply with payload '" + resultArg + "'", ex);
else {
doHandleResult(resultArg, request, channel, source);
}
}
else if (this.logger.isWarnEnabled()) {
Expand All @@ -321,6 +328,43 @@ else if (this.logger.isWarnEnabled()) {
}
}

private void asyncSuccess(InvocationResult resultArg, Message request, Channel channel, Object source, Object r) {
doHandleResult(new InvocationResult(r, resultArg.getSendTo(), resultArg.getReturnType()), request,
channel, source);
try {
channel.basicAck(request.getMessageProperties().getDeliveryTag(), false);
}
catch (IOException e) {
this.logger.error("Failed to nack message", e);
}
}

private void asyncFailure(Message request, Channel channel, Throwable t) {
this.logger.error("Future was completed with an exception for " + request, t);
try {
channel.basicNack(request.getMessageProperties().getDeliveryTag(), false, true);
}
catch (IOException e) {
this.logger.error("Failed to nack message", e);
}
}

protected void doHandleResult(InvocationResult resultArg, Message request, Channel channel, Object source) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Listener method returned result [" + resultArg
+ "] - generating response message for it");
}
try {
Message response = buildMessage(channel, resultArg.getReturnValue(), resultArg.getReturnType());
postProcessResponse(request, response);
Address replyTo = getReplyToAddress(request, source, resultArg);
sendResponse(channel, replyTo, response);
}
catch (Exception ex) {
throw new ReplyFailureException("Failed to send reply with payload '" + resultArg + "'", ex);
}
}

protected String getReceivedExchange(Message request) {
return request.getMessageProperties().getReceivedExchange();
}
Expand Down Expand Up @@ -517,4 +561,19 @@ public Object getResult() {

}

private static class MonoHandler {

static boolean isMono(Object result) {
return result instanceof Mono;
}

@SuppressWarnings("unchecked")
static void subscribe(Object returnValue, Consumer<? super Object> success,
Consumer<? super Throwable> failure) {

((Mono<? super Object>) returnValue).subscribe(success, failure);
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* Copyright 2018 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.annotation;

import static org.junit.Assert.assertEquals;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.core.AnonymousQueue;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.AsyncRabbitTemplate;
import org.springframework.amqp.rabbit.AsyncRabbitTemplate.RabbitConverterFuture;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
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.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
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.SettableListenableFuture;

import reactor.core.publisher.Mono;

/**
* @author Gary Russell
* @since 2.1
*
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
public class AsyncListenerTests {

@Rule
public BrokerRunning brokerRunning = BrokerRunning.isRunning();

@Autowired
private RabbitTemplate rabbitTemplate;

@Autowired
private AsyncRabbitTemplate asyncTemplate;

@Autowired
private Queue queue1;

@Autowired
private Queue queue2;

@Test
public void testAsyncListener() throws Exception {
assertEquals("FOO", this.rabbitTemplate.convertSendAndReceive(this.queue1.getName(), "foo"));
RabbitConverterFuture<Object> future = this.asyncTemplate.convertSendAndReceive(this.queue1.getName(), "foo");
assertEquals("FOO", future.get(10, TimeUnit.SECONDS));
assertEquals("FOO", this.rabbitTemplate.convertSendAndReceive(this.queue2.getName(), "foo"));
}

@Configuration
@EnableRabbit
public static class EnableRabbitConfig {

@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(rabbitConnectionFactory());
factory.setMismatchedQueuesFatal(true);
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
return factory;
}

@Bean
public ConnectionFactory rabbitConnectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setHost("localhost");
return connectionFactory;
}

@Bean
public RabbitTemplate rabbitTemplate() {
return new RabbitTemplate(rabbitConnectionFactory());
}

@Bean
public AsyncRabbitTemplate asyncTemplate() {
return new AsyncRabbitTemplate(rabbitTemplate());
}

@Bean
public RabbitAdmin rabbitAdmin() {
return new RabbitAdmin(rabbitConnectionFactory());
}

@Bean
public Queue queue1() {
return new AnonymousQueue();
}

@Bean
public Queue queue2() {
return new AnonymousQueue();
}

@Bean
public Listener listener() {
return new Listener();
}

}

@Component
public static class Listener {

private final AtomicBoolean fooFirst = new AtomicBoolean(true);

private final AtomicBoolean barFirst = new AtomicBoolean(true);

@RabbitListener(id = "foo", queues = "#{queue1.name}")
public ListenableFuture<String> listen1(String foo) {
SettableListenableFuture<String> future = new SettableListenableFuture<>();
if (fooFirst.getAndSet(false)) {
future.setException(new RuntimeException("Future.exception"));
}
else {
future.set(foo.toUpperCase());
}
return future;
}

@RabbitListener(id = "bar", queues = "#{queue2.name}")
public Mono<String> listen2(String foo) {
if (barFirst.getAndSet(false)) {
return Mono.error(new RuntimeException("Mono.error()"));
}
else {
return Mono.just(foo.toUpperCase());
}
}

}

}
8 changes: 8 additions & 0 deletions src/reference/asciidoc/amqp.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2555,6 +2555,14 @@ These techniques are useful if you wish to create several containers with simila

IMPORTANT: Containers created this way are normal `@Bean` s and are not registered in the `RabbitListenerEndpointRegistry`.

[[async-returns]]
===== Asynchronous @RabbitListener Return Types

Starting with version 2.1, `@RabbitListener` (and `@RabbitHandler`) methods can be specified with asynchronous return types `ListenableFuture<?>` and `Mono<?>`, allowing the reply to be sent asynchronously.

IMPORTANT: The listener container factory must be configured with `AcknowledgeMode.MANUAL` so that the consumer thread will not ack the message; instead, the asynchronous completion will ack or nack (requeue) the message when the async operation completes.
If some exception occurs within the listener method that prevents creation of the async result object, you MUST catch that exception and return an appropriate return object that will cause the message to be acknowledged or requeued.

[[threading]]
===== Threading and Asynchronous Consumers

Expand Down
5 changes: 5 additions & 0 deletions src/reference/asciidoc/whats-new.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ See <<management-rest-api>> for more information.
The listener container factory can now be configured with a `RetryTemplate` and, optionally, a `RecoveryCallback` used when sending replies.
See <<async-annotation-driven-enable>> for more information.

===== Async @RabbitListener Return

`@RabbitListener` methods can now return `ListenableFuture<?>` or `Mono<?>`.
See <<async-return>> for more information.

===== Connection Factory Bean Changes

The `RabbitConnectionFactoryBean` now calls `enableHostnameVerification()` by default; to revert to the previous behavior, set the `enabaleHostnameVerification` property to `false`.

0 comments on commit e9d553a

Please sign in to comment.