GH-3635: Add Future<Void> & Mono<Void> to gateway (#3899)
* GH-3635: Add Future<Void> & Mono<Void> to gateway Fixes https://github.com/spring-projects/spring-integration/issues/3635 When `Future<Void>` & `Mono<Void>` is used as a messaging gateway return type, the application hangs out on this barrier which may lead to the out of memory eventually * Add support for the `Future<Void>` & `Mono<Void>` messaging gateway return type and ensure an asynchronous call for the `gateway.send(Message)` operation and its exception handling. In case of successful call, the `Future` is fulfilled with `null` and `Mono` is completed as empty * * Check for `void.class` as well in the `GatewayProxyFactoryBean.isVoidReturnType` * * Allow `Future<Void>` as a reply type of the gateway request-reply operation * * Fix Checkstyle violations * * Resolve `System.err.println()` in the test code * Add `Thread.currentThread.interrupt()` to the `InterruptedException` block in the test
This commit is contained in:
@@ -564,15 +564,16 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
|
||||
}
|
||||
boolean shouldReturnMessage =
|
||||
Message.class.isAssignableFrom(gateway.returnType) || (!runningOnCallerThread && gateway.expectMessage);
|
||||
boolean shouldReply = gateway.returnType != void.class;
|
||||
boolean oneWay =
|
||||
void.class.isAssignableFrom(gateway.returnType) || (gateway.isVoidReturn && !runningOnCallerThread);
|
||||
int paramCount = method.getParameterTypes().length;
|
||||
Object response;
|
||||
boolean hasPayloadExpression = findPayloadExpression(method);
|
||||
if (paramCount == 0 && !hasPayloadExpression) {
|
||||
response = receive(gateway, method, shouldReply, shouldReturnMessage);
|
||||
response = receive(gateway, method, !oneWay, shouldReturnMessage);
|
||||
}
|
||||
else {
|
||||
response = sendOrSendAndReceive(invocation, gateway, shouldReturnMessage, shouldReply);
|
||||
response = sendOrSendAndReceive(invocation, gateway, shouldReturnMessage, !oneWay);
|
||||
}
|
||||
return response(gateway.returnType, shouldReturnMessage, response);
|
||||
}
|
||||
@@ -640,7 +641,12 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
|
||||
}
|
||||
}
|
||||
else {
|
||||
gateway.send(args);
|
||||
if (gateway.isMonoReturn) {
|
||||
return Mono.fromRunnable(() -> gateway.send(args));
|
||||
}
|
||||
else {
|
||||
gateway.send(args);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -30,18 +30,24 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.DirectFieldAccessor;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.integration.MessageDispatchingException;
|
||||
import org.springframework.integration.annotation.Gateway;
|
||||
import org.springframework.integration.annotation.GatewayHeader;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.channel.NullChannel;
|
||||
import org.springframework.integration.channel.QueueChannel;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
@@ -70,7 +76,7 @@ public class AsyncGatewayTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void futureWithError() throws Exception {
|
||||
public void futureWithError() {
|
||||
final Error error = new Error("error");
|
||||
DirectChannel channel = new DirectChannel() {
|
||||
|
||||
@@ -140,7 +146,7 @@ public class AsyncGatewayTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonAsyncFutureReturned() throws Exception {
|
||||
public void nonAsyncFutureReturned() {
|
||||
QueueChannel requestChannel = new QueueChannel();
|
||||
addThreadEnricher(requestChannel);
|
||||
startResponder(requestChannel);
|
||||
@@ -204,6 +210,60 @@ public class AsyncGatewayTests {
|
||||
assertThat(result).isEqualTo("foobar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void futureVoid() throws Exception {
|
||||
GatewayProxyFactoryBean proxyFactory = new GatewayProxyFactoryBean(TestEchoService.class);
|
||||
proxyFactory.setDefaultRequestChannel(new NullChannel());
|
||||
proxyFactory.setBeanName("testGateway");
|
||||
proxyFactory.setBeanFactory(mock(BeanFactory.class));
|
||||
proxyFactory.afterPropertiesSet();
|
||||
TestEchoService service = (TestEchoService) proxyFactory.getObject();
|
||||
Future<Void> f = service.asyncSendAndForget("test1");
|
||||
Object result = f.get(10, TimeUnit.SECONDS);
|
||||
assertThat(result).isNull();
|
||||
|
||||
new DirectFieldAccessor(proxyFactory).setPropertyValue("initialized", false);
|
||||
proxyFactory.setDefaultRequestChannel((message, timeout) -> {
|
||||
throw new MessageDispatchingException(message, "intentional dispatcher error");
|
||||
});
|
||||
proxyFactory.afterPropertiesSet();
|
||||
|
||||
Future<Void> futureError = service.asyncSendAndForget("test2");
|
||||
assertThatExceptionOfType(ExecutionException.class)
|
||||
.isThrownBy(() -> futureError.get(10, TimeUnit.SECONDS))
|
||||
.withCauseInstanceOf(MessageDispatchingException.class)
|
||||
.withMessageContaining("intentional dispatcher error");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void futureVoidReply() throws Exception {
|
||||
QueueChannel requestChannel = new QueueChannel();
|
||||
CountDownLatch readyForReplyLatch = new CountDownLatch(1);
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Message<?> input = requestChannel.receive();
|
||||
CompletableFuture<Void> reply = new CompletableFuture<>();
|
||||
((MessageChannel) input.getHeaders().getReplyChannel()).send(new GenericMessage<>(reply));
|
||||
readyForReplyLatch.await(10, TimeUnit.SECONDS);
|
||||
reply.complete(null);
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread.interrupt();
|
||||
ReflectionUtils.rethrowRuntimeException(ex);
|
||||
}
|
||||
}).start();
|
||||
GatewayProxyFactoryBean proxyFactory = new GatewayProxyFactoryBean(TestEchoService.class);
|
||||
proxyFactory.setDefaultRequestChannel(requestChannel);
|
||||
proxyFactory.setBeanName("testGateway");
|
||||
proxyFactory.setBeanFactory(mock(BeanFactory.class));
|
||||
proxyFactory.setAsyncExecutor(null);
|
||||
proxyFactory.afterPropertiesSet();
|
||||
TestEchoService service = (TestEchoService) proxyFactory.getObject();
|
||||
Future<Void> f = service.sendAndReceiveFutureVoid("test");
|
||||
readyForReplyLatch.countDown();
|
||||
Object result = f.get(10, TimeUnit.SECONDS);
|
||||
assertThat(result).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void monoWithMessageReturned() {
|
||||
@@ -252,7 +312,7 @@ public class AsyncGatewayTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void monoWithConsumer() throws Exception {
|
||||
public void monoWithConsumer() {
|
||||
QueueChannel requestChannel = new QueueChannel();
|
||||
startResponder(requestChannel);
|
||||
GatewayProxyFactoryBean proxyFactory = new GatewayProxyFactoryBean(TestEchoService.class);
|
||||
@@ -263,16 +323,38 @@ public class AsyncGatewayTests {
|
||||
TestEchoService service = (TestEchoService) proxyFactory.getObject();
|
||||
Mono<String> mono = service.returnStringPromise("foo");
|
||||
|
||||
final AtomicReference<String> result = new AtomicReference<>();
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
StepVerifier.create(mono)
|
||||
.expectNext("foobar")
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
mono.subscribe(s -> {
|
||||
result.set(s);
|
||||
latch.countDown();
|
||||
@Test
|
||||
public void monoVoid() throws InterruptedException {
|
||||
GatewayProxyFactoryBean proxyFactory = new GatewayProxyFactoryBean(TestEchoService.class);
|
||||
proxyFactory.setDefaultRequestChannel(new NullChannel());
|
||||
proxyFactory.setBeanName("testGateway");
|
||||
proxyFactory.setBeanFactory(mock(BeanFactory.class));
|
||||
proxyFactory.afterPropertiesSet();
|
||||
TestEchoService service = (TestEchoService) proxyFactory.getObject();
|
||||
Mono<Void> mono = service.monoVoid("test1");
|
||||
|
||||
CountDownLatch emptyMonoLatch = new CountDownLatch(1);
|
||||
mono.switchIfEmpty(Mono.empty().doOnSuccess(v -> emptyMonoLatch.countDown()).then()).subscribe();
|
||||
|
||||
assertThat(emptyMonoLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
|
||||
new DirectFieldAccessor(proxyFactory).setPropertyValue("initialized", false);
|
||||
proxyFactory.setDefaultRequestChannel((message, timeout) -> {
|
||||
throw new MessageDispatchingException(message, "intentional dispatcher error");
|
||||
});
|
||||
proxyFactory.afterPropertiesSet();
|
||||
|
||||
latch.await(10, TimeUnit.SECONDS);
|
||||
assertThat(result.get()).isEqualTo("foobar");
|
||||
Mono<Void> monoError = service.monoVoid("test2");
|
||||
|
||||
StepVerifier.create(monoError)
|
||||
.expectSubscription()
|
||||
.expectError(MessageDispatchingException.class)
|
||||
.verify(Duration.ofSeconds(10));
|
||||
}
|
||||
|
||||
private static void startResponder(final PollableChannel requestChannel) {
|
||||
@@ -323,18 +405,15 @@ public class AsyncGatewayTests {
|
||||
|
||||
Mono<?> returnSomethingPromise(String s);
|
||||
|
||||
Future<Void> asyncSendAndForget(String s);
|
||||
|
||||
Future<Void> sendAndReceiveFutureVoid(String s);
|
||||
|
||||
Mono<Void> monoVoid(String s);
|
||||
|
||||
}
|
||||
|
||||
private static class CustomFuture implements Future<String> {
|
||||
|
||||
private final String result;
|
||||
|
||||
private final Thread thread;
|
||||
|
||||
private CustomFuture(String result, Thread thread) {
|
||||
this.result = result;
|
||||
this.thread = thread;
|
||||
}
|
||||
private record CustomFuture(String result, Thread thread) implements Future<String> {
|
||||
|
||||
@Override
|
||||
public boolean cancel(boolean mayInterruptIfRunning) {
|
||||
|
||||
@@ -460,7 +460,7 @@ If you provide a one-way flow, nothing would be sent back to the caller.
|
||||
If you want to completely suppress exceptions, you can provide a reference to the global `nullChannel` (essentially a `/dev/null` approach).
|
||||
Finally, as mentioned above, if no `error-channel` is defined, then the exceptions propagate as usual.
|
||||
|
||||
When you use the `@MessagingGateway` annotation (see `<<messaging-gateway-annotation>>`), you can use use the `errorChannel` attribute.
|
||||
When you use the `@MessagingGateway` annotation (see `<<messaging-gateway-annotation>>`), you can use an `errorChannel` attribute.
|
||||
|
||||
Starting with version 5.0, when you use a gateway method with a `void` return type (one-way flow), the `error-channel` reference (if provided) is populated in the standard `errorChannel` header of each sent message.
|
||||
This feature allows a downstream asynchronous flow, based on the standard `ExecutorChannel` configuration (or a `QueueChannel`), to override a default global `errorChannel` exceptions sending behavior.
|
||||
@@ -469,7 +469,7 @@ The `error-channel` property was ignored for `void` methods with an asynchronous
|
||||
Instead, error messages were sent to the default `errorChannel`.
|
||||
|
||||
|
||||
IMPORTANT: Exposing the messaging system through simple POJI Gateways provides benefits, but "`hiding`" the reality of the underlying messaging system does come at a price, so there are certain things you should consider.
|
||||
IMPORTANT: Exposing the messaging system through simple POJI Gateways provides benefits, but "`hiding`" the reality of the underlying messaging system does come at a price, so there are certain things you should consider.
|
||||
We want our Java method to return as quickly as possible and not hang for an indefinite amount of time while the caller is waiting on it to return (whether void, a return value, or a thrown Exception).
|
||||
When regular methods are used as a proxies in front of the messaging system, we have to take into account the potentially asynchronous nature of the underlying messaging.
|
||||
This means that there might be a chance that a message that was initiated by a gateway could be dropped by a filter and never reach a component that is responsible for producing a reply.
|
||||
@@ -782,10 +782,9 @@ The calling thread continues, with `handleInvoice()` being called when the flow
|
||||
As mentioned in the <<gateway-asynctaskexecutor>> section above, if you wish some downstream component to return a message with an async payload (`Future`, `Mono`, and others), you must explicitly set the async executor to `null` (or `""` when using XML configuration).
|
||||
The flow is then invoked on the caller thread and the result can be retrieved later.
|
||||
|
||||
===== `void` Return Type
|
||||
===== Asynchronous `void` Return Type
|
||||
|
||||
Unlike the return types mentioned earlier, when the method return type is `void`, the framework cannot implicitly determine that you wish the downstream flow to run asynchronously, with the caller thread returning immediately.
|
||||
In this case, you must annotate the interface method with `@Async`, as the following example shows:
|
||||
The messaging gateway method can be declared like this:
|
||||
|
||||
====
|
||||
[source, java]
|
||||
@@ -801,7 +800,15 @@ public interface MyGateway {
|
||||
----
|
||||
====
|
||||
|
||||
Unlike the `Future<?>` return types, there is no way to inform the caller if some exception is thrown by the flow, unless some custom `TaskExecutor` (such as an `ErrorHandlingTaskExecutor`) is associated with the `@Async` annotation.
|
||||
But downstream exceptions are not going to be propagated back to the caller.
|
||||
To ensure asynchronous behavior for downstream flow invocation and exception propagation to the caller, starting with version 6.0, the framework provides support for the `Future<Void>` and `Mono<Void>` return types.
|
||||
The use-case is similar to send-and-forget behavior described before for plain `void` return type, but with a difference that flow execution happens asynchronously and returned `Future` (or `Mono`) is complete with a `null` or exceptionally according to the `send` operation result.
|
||||
|
||||
NOTE: If the `Future<Void>` is exact downstream flow reply, then an `asyncExecutor` option of the gateway must be set to null (`AnnotationConstants.NULL` for a `@MessagingGateway` configuration) and the `send` part is performed on a producer thread.
|
||||
The reply one depends on the downstream flow configuration.
|
||||
This way it is up target application to produce a `Future<Void>` reply correctly.
|
||||
The `Mono` use-case is already out of the framework threading control, so setting `asyncExecutor` to null won't make sense.
|
||||
There `Mono<Void>` as a result of the request-reply gateway operation must be configured as a `Mono<?>` return type of the gateway method.
|
||||
|
||||
[[gateway-no-response]]
|
||||
==== Gateway Behavior When No response Arrives
|
||||
|
||||
@@ -80,7 +80,9 @@ See <<./dsl.adoc#java-dsl,Java DSL>> for more information.
|
||||
The `org.springframework.util.concurrent.ListenableFuture` has been deprecated starting with Spring Framework `6.0`.
|
||||
All Spring Integration async API has been migrated to the `CompletableFuture`.
|
||||
|
||||
See <<./gateway.adoc#gw-completable-future, CompletableFuture support>> for more information.
|
||||
Also Messaging Gateway interface method can now return `Future<Void>` and `Mono<Void>` with a proper asynchronous execution of the downstream flow.
|
||||
|
||||
See <<./gateway.adoc#async-gateway, Asynchronous Gateway>> for more information.
|
||||
|
||||
The `integrationGlobalProperties` bean is now declared by the framework as an instance of `org.springframework.integration.context.IntegrationProperties` instead of the previously deprecated `java.util.Properties`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user