GH-8705 Expose errorOnTimeout on MessagingGateway

Fixes https://github.com/spring-projects/spring-integration/issues/8705

an internal `MethodInvocationGateway` is a `MessagingGatewaySupport`
extension with all the logic available there.
One of the option introduced in `5.2.2` to be able to throw a `MessageTimeoutException`
instead of returning `null` when no reply received in time from downstream flow

* Expose an `errorOnTimeout` on the `@MessagingGateway` and `GatewayEndpointSpec`
* Propagate this option from a `GatewayProxyFactoryBean` down to its internal
`MethodInvocationGateway` implementation
* Modify couple tests to react for `errorOnTimeout` set to `true`
* Document the feature

Fix language in Docs

Co-authored-by: Gary Russell <grussell@vmware.com>
This commit is contained in:
Artem Bilan
2023-08-18 13:46:28 -07:00
parent d85c5e3a0a
commit 4e310bbad6
9 changed files with 85 additions and 12 deletions

View File

@@ -23,6 +23,7 @@ import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.gateway.MessagingGatewaySupport;
/**
* A stereotype annotation to provide an Integration Messaging Gateway Proxy
@@ -165,4 +166,13 @@ public @interface MessagingGateway {
*/
boolean proxyDefaultMethods() default false;
/**
* If errorOnTimeout is true, null won't be returned as a result of a gateway method invocation when a timeout occurs.
* Instead, a {@link org.springframework.integration.MessageTimeoutException} is thrown
* or an error message is published to the error channel.
* @since 6.2
* @see MessagingGatewaySupport#setErrorOnTimeout(boolean)
*/
boolean errorOnTimeout() default false;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2020 the original author or authors.
* Copyright 2016-2023 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.
@@ -98,4 +98,16 @@ public class GatewayEndpointSpec extends ConsumerEndpointSpec<GatewayEndpointSpe
return this;
}
/**
* Set a error on timeout flag.
* @param errorOnTimeout true to produce an error in case of a reply timeout.
* @return the spec.
* @since 6.2
* @see org.springframework.integration.gateway.GatewayProxyFactoryBean#setErrorOnTimeout(boolean)
*/
public GatewayEndpointSpec errorOnTimeout(boolean errorOnTimeout) {
this.handler.setErrorOnTimeout(errorOnTimeout);
return this;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2022 the original author or authors.
* Copyright 2017-2023 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.
@@ -128,6 +128,8 @@ public class AnnotationGatewayProxyFactoryBean<T> extends GatewayProxyFactoryBea
populateAsyncExecutorIfAny();
setErrorOnTimeout(this.gatewayAttributes.getBoolean("errorOnTimeout"));
boolean proxyDefaultMethods = this.gatewayAttributes.getBoolean("proxyDefaultMethods");
if (proxyDefaultMethods) { // Override only if annotation attribute is different
setProxyDefaultMethods(true);

View File

@@ -81,6 +81,10 @@ public class GatewayMessageHandler extends AbstractReplyProducingMessageHandler
this.gatewayProxyFactoryBean.setDefaultReplyTimeout(replyTimeout);
}
public void setErrorOnTimeout(boolean errorOnTimeout) {
this.gatewayProxyFactoryBean.setErrorOnTimeout(errorOnTimeout);
}
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
if (this.exchanger == null) {

View File

@@ -166,6 +166,8 @@ public class GatewayProxyFactoryBean<T> extends AbstractEndpoint
private MetricsCaptor metricsCaptor;
private boolean errorOnTimeout;
/**
* Create a Factory whose service interface type can be configured by setter injection.
* If none is set, it will fall back to the default service interface type,
@@ -455,6 +457,18 @@ public class GatewayProxyFactoryBean<T> extends AbstractEndpoint
this.gatewayMap.values().forEach(gw -> gw.registerMetricsCaptor(metricsCaptorToRegister));
}
/**
* If errorOnTimeout is true, null won't be returned as a result of a gateway method invocation, when a timeout occurs.
* Instead, a {@link org.springframework.integration.MessageTimeoutException} is thrown
* or an error message is published to the error channel.
* @param errorOnTimeout true to create the error message on reply timeout.
* @since 6.2
* @see MessagingGatewaySupport#setErrorOnTimeout(boolean)
*/
public void setErrorOnTimeout(boolean errorOnTimeout) {
this.errorOnTimeout = errorOnTimeout;
}
@Override
@SuppressWarnings("unchecked")
protected void onInit() {
@@ -881,6 +895,7 @@ public class GatewayProxyFactoryBean<T> extends AbstractEndpoint
gateway.setBeanFactory(getBeanFactory());
gateway.setShouldTrack(this.shouldTrack);
gateway.registerMetricsCaptor(this.metricsCaptor);
gateway.setErrorOnTimeout(this.errorOnTimeout);
gateway.afterPropertiesSet();
return gateway;

View File

@@ -31,6 +31,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.integration.MessageRejectedException;
import org.springframework.integration.MessageTimeoutException;
import org.springframework.integration.annotation.Gateway;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.config.EnableIntegration;
@@ -43,6 +44,7 @@ import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.messaging.support.GenericMessage;
@@ -79,9 +81,11 @@ public class GatewayDslTests {
assertThat(receive.getPayload()).isEqualTo("From Gateway SubFlow: FOO");
assertThat(this.gatewayError.receive(1)).isNull();
message = MessageBuilder.withPayload("bar").setReplyChannel(replyChannel).build();
Message<String> otherMessage = MessageBuilder.withPayload("bar").setReplyChannel(replyChannel).build();
this.gatewayInput.send(message);
assertThatExceptionOfType(MessageHandlingException.class)
.isThrownBy(() -> this.gatewayInput.send(otherMessage))
.withCauseExactlyInstanceOf(MessageTimeoutException.class);
assertThat(replyChannel.receive(1)).isNull();
@@ -173,7 +177,8 @@ public class GatewayDslTests {
@Bean
public IntegrationFlow gatewayFlow() {
return IntegrationFlow.from("gatewayInput")
.gateway("gatewayRequest", g -> g.errorChannel("gatewayError").replyTimeout(10L))
.gateway("gatewayRequest",
g -> g.errorChannel("gatewayError").replyTimeout(10L).errorOnTimeout(true))
.gateway((f) -> f.transform("From Gateway SubFlow: "::concat))
.get();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@@ -59,14 +59,17 @@ import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.MessageTimeoutException;
import org.springframework.integration.annotation.AnnotationConstants;
import org.springframework.integration.annotation.BridgeTo;
import org.springframework.integration.annotation.Gateway;
import org.springframework.integration.annotation.GatewayHeader;
import org.springframework.integration.annotation.IntegrationComponentScan;
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.annotation.Poller;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.context.IntegrationProperties;
@@ -93,6 +96,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
@@ -150,6 +154,9 @@ public class GatewayInterfaceTests {
@Autowired
private MessageChannel gatewayChannel;
@Autowired
private PollableChannel gatewayQueueChannel;
@Autowired
private MessageChannel errorChannel;
@@ -468,19 +475,19 @@ public class GatewayInterfaceTests {
assertThat(TestUtils.getPropertyValue(this.annotationGatewayProxyFactoryBean,
"defaultRequestTimeout", Expression.class).getValue()).isEqualTo(1111L);
assertThat(TestUtils.getPropertyValue(this.annotationGatewayProxyFactoryBean,
"defaultReplyTimeout", Expression.class).getValue()).isEqualTo(222L);
"defaultReplyTimeout", Expression.class).getValue()).isEqualTo(0L);
Collection<MessagingGatewaySupport> messagingGateways =
this.annotationGatewayProxyFactoryBean.getGateways().values();
assertThat(messagingGateways.size()).isEqualTo(1);
MessagingGatewaySupport gateway = messagingGateways.iterator().next();
assertThat(gateway.getRequestChannel()).isSameAs(this.gatewayChannel);
assertThat(gateway.getRequestChannel()).isSameAs(this.gatewayQueueChannel);
assertThat(gateway.getReplyChannel()).isSameAs(this.gatewayChannel);
assertThat(gateway.getErrorChannel()).isSameAs(this.errorChannel);
Object requestMapper = TestUtils.getPropertyValue(gateway, "requestMapper");
assertThat(TestUtils.getPropertyValue(requestMapper, "payloadExpression.expression")).isEqualTo("@foo");
assertThat(TestUtils.getPropertyValue(requestMapper, "payloadExpression.expression")).isEqualTo("args[0]");
Map globalHeaderExpressions = TestUtils.getPropertyValue(requestMapper, "globalHeaderExpressions", Map.class);
assertThat(globalHeaderExpressions.size()).isEqualTo(1);
@@ -489,6 +496,9 @@ public class GatewayInterfaceTests {
assertThat(barHeaderExpression).isNotNull();
assertThat(barHeaderExpression).isInstanceOf(LiteralExpression.class);
assertThat(((LiteralExpression) barHeaderExpression).getValue()).isEqualTo("baz");
assertThatExceptionOfType(MessageTimeoutException.class)
.isThrownBy(() -> this.gatewayByAnnotationGPFB.foo("test"));
}
@Test
@@ -667,6 +677,12 @@ public class GatewayInterfaceTests {
return new DirectChannel();
}
@Bean
@BridgeTo(poller = @Poller(fixedDelay = "1000"))
public MessageChannel gatewayQueueChannel() {
return new QueueChannel();
}
@Bean
@BridgeTo
public MessageChannel gatewayThreadChannel() {
@@ -802,13 +818,14 @@ public class GatewayInterfaceTests {
}
@MessagingGateway(
defaultRequestChannel = "${gateway.channel:gatewayChannel}",
defaultRequestChannel = "${gateway.channel:gatewayQueueChannel}",
defaultReplyChannel = "${gateway.channel:gatewayChannel}",
defaultPayloadExpression = "${gateway.payload:@foo}",
defaultPayloadExpression = "${gateway.payload:args[0]}",
errorChannel = "${gateway.channel:errorChannel}",
asyncExecutor = "${gateway.executor:exec}",
defaultRequestTimeout = "${gateway.timeout:1111}",
defaultReplyTimeout = "${gateway.timeout:222}",
defaultReplyTimeout = "${gateway.timeout:0}",
errorOnTimeout = true,
defaultHeaders = {
@GatewayHeader(name = "${gateway.header.name:bar}",
value = "${gateway.header.value:baz}")