Skip to content

Commit 4e310bb

Browse files
GH-8705 Expose errorOnTimeout on MessagingGateway
Fixes #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 <[email protected]>
1 parent d85c5e3 commit 4e310bb

File tree

9 files changed

+85
-12
lines changed

9 files changed

+85
-12
lines changed

spring-integration-core/src/main/java/org/springframework/integration/annotation/MessagingGateway.java

+10
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import org.springframework.core.annotation.AliasFor;
2525
import org.springframework.integration.context.IntegrationContextUtils;
26+
import org.springframework.integration.gateway.MessagingGatewaySupport;
2627

2728
/**
2829
* A stereotype annotation to provide an Integration Messaging Gateway Proxy
@@ -165,4 +166,13 @@
165166
*/
166167
boolean proxyDefaultMethods() default false;
167168

169+
/**
170+
* If errorOnTimeout is true, null won't be returned as a result of a gateway method invocation when a timeout occurs.
171+
* Instead, a {@link org.springframework.integration.MessageTimeoutException} is thrown
172+
* or an error message is published to the error channel.
173+
* @since 6.2
174+
* @see MessagingGatewaySupport#setErrorOnTimeout(boolean)
175+
*/
176+
boolean errorOnTimeout() default false;
177+
168178
}

spring-integration-core/src/main/java/org/springframework/integration/dsl/GatewayEndpointSpec.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2020 the original author or authors.
2+
* Copyright 2016-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -98,4 +98,16 @@ public GatewayEndpointSpec replyTimeout(Long replyTimeout) {
9898
return this;
9999
}
100100

101+
/**
102+
* Set a error on timeout flag.
103+
* @param errorOnTimeout true to produce an error in case of a reply timeout.
104+
* @return the spec.
105+
* @since 6.2
106+
* @see org.springframework.integration.gateway.GatewayProxyFactoryBean#setErrorOnTimeout(boolean)
107+
*/
108+
public GatewayEndpointSpec errorOnTimeout(boolean errorOnTimeout) {
109+
this.handler.setErrorOnTimeout(errorOnTimeout);
110+
return this;
111+
}
112+
101113
}

spring-integration-core/src/main/java/org/springframework/integration/gateway/AnnotationGatewayProxyFactoryBean.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2022 the original author or authors.
2+
* Copyright 2017-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -128,6 +128,8 @@ protected void onInit() {
128128

129129
populateAsyncExecutorIfAny();
130130

131+
setErrorOnTimeout(this.gatewayAttributes.getBoolean("errorOnTimeout"));
132+
131133
boolean proxyDefaultMethods = this.gatewayAttributes.getBoolean("proxyDefaultMethods");
132134
if (proxyDefaultMethods) { // Override only if annotation attribute is different
133135
setProxyDefaultMethods(true);

spring-integration-core/src/main/java/org/springframework/integration/gateway/GatewayMessageHandler.java

+4
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ public void setReplyTimeout(Long replyTimeout) {
8181
this.gatewayProxyFactoryBean.setDefaultReplyTimeout(replyTimeout);
8282
}
8383

84+
public void setErrorOnTimeout(boolean errorOnTimeout) {
85+
this.gatewayProxyFactoryBean.setErrorOnTimeout(errorOnTimeout);
86+
}
87+
8488
@Override
8589
protected Object handleRequestMessage(Message<?> requestMessage) {
8690
if (this.exchanger == null) {

spring-integration-core/src/main/java/org/springframework/integration/gateway/GatewayProxyFactoryBean.java

+15
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ public class GatewayProxyFactoryBean<T> extends AbstractEndpoint
166166

167167
private MetricsCaptor metricsCaptor;
168168

169+
private boolean errorOnTimeout;
170+
169171
/**
170172
* Create a Factory whose service interface type can be configured by setter injection.
171173
* If none is set, it will fall back to the default service interface type,
@@ -455,6 +457,18 @@ public void registerMetricsCaptor(MetricsCaptor metricsCaptorToRegister) {
455457
this.gatewayMap.values().forEach(gw -> gw.registerMetricsCaptor(metricsCaptorToRegister));
456458
}
457459

460+
/**
461+
* If errorOnTimeout is true, null won't be returned as a result of a gateway method invocation, when a timeout occurs.
462+
* Instead, a {@link org.springframework.integration.MessageTimeoutException} is thrown
463+
* or an error message is published to the error channel.
464+
* @param errorOnTimeout true to create the error message on reply timeout.
465+
* @since 6.2
466+
* @see MessagingGatewaySupport#setErrorOnTimeout(boolean)
467+
*/
468+
public void setErrorOnTimeout(boolean errorOnTimeout) {
469+
this.errorOnTimeout = errorOnTimeout;
470+
}
471+
458472
@Override
459473
@SuppressWarnings("unchecked")
460474
protected void onInit() {
@@ -881,6 +895,7 @@ private MethodInvocationGateway doCreateMethodInvocationGateway(Method method,
881895
gateway.setBeanFactory(getBeanFactory());
882896
gateway.setShouldTrack(this.shouldTrack);
883897
gateway.registerMetricsCaptor(this.metricsCaptor);
898+
gateway.setErrorOnTimeout(this.errorOnTimeout);
884899
gateway.afterPropertiesSet();
885900

886901
return gateway;

spring-integration-core/src/test/java/org/springframework/integration/dsl/gateway/GatewayDslTests.java

+8-3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.context.annotation.Configuration;
3232
import org.springframework.core.task.TaskExecutor;
3333
import org.springframework.integration.MessageRejectedException;
34+
import org.springframework.integration.MessageTimeoutException;
3435
import org.springframework.integration.annotation.Gateway;
3536
import org.springframework.integration.channel.QueueChannel;
3637
import org.springframework.integration.config.EnableIntegration;
@@ -43,6 +44,7 @@
4344
import org.springframework.integration.test.util.TestUtils;
4445
import org.springframework.messaging.Message;
4546
import org.springframework.messaging.MessageChannel;
47+
import org.springframework.messaging.MessageHandlingException;
4648
import org.springframework.messaging.PollableChannel;
4749
import org.springframework.messaging.support.ErrorMessage;
4850
import org.springframework.messaging.support.GenericMessage;
@@ -79,9 +81,11 @@ void testGatewayFlow() {
7981
assertThat(receive.getPayload()).isEqualTo("From Gateway SubFlow: FOO");
8082
assertThat(this.gatewayError.receive(1)).isNull();
8183

82-
message = MessageBuilder.withPayload("bar").setReplyChannel(replyChannel).build();
84+
Message<String> otherMessage = MessageBuilder.withPayload("bar").setReplyChannel(replyChannel).build();
8385

84-
this.gatewayInput.send(message);
86+
assertThatExceptionOfType(MessageHandlingException.class)
87+
.isThrownBy(() -> this.gatewayInput.send(otherMessage))
88+
.withCauseExactlyInstanceOf(MessageTimeoutException.class);
8589

8690
assertThat(replyChannel.receive(1)).isNull();
8791

@@ -173,7 +177,8 @@ public static class ContextConfiguration {
173177
@Bean
174178
public IntegrationFlow gatewayFlow() {
175179
return IntegrationFlow.from("gatewayInput")
176-
.gateway("gatewayRequest", g -> g.errorChannel("gatewayError").replyTimeout(10L))
180+
.gateway("gatewayRequest",
181+
g -> g.errorChannel("gatewayError").replyTimeout(10L).errorOnTimeout(true))
177182
.gateway((f) -> f.transform("From Gateway SubFlow: "::concat))
178183
.get();
179184
}

spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayInterfaceTests.java

+24-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -59,14 +59,17 @@
5959
import org.springframework.core.type.AnnotatedTypeMetadata;
6060
import org.springframework.expression.Expression;
6161
import org.springframework.expression.common.LiteralExpression;
62+
import org.springframework.integration.MessageTimeoutException;
6263
import org.springframework.integration.annotation.AnnotationConstants;
6364
import org.springframework.integration.annotation.BridgeTo;
6465
import org.springframework.integration.annotation.Gateway;
6566
import org.springframework.integration.annotation.GatewayHeader;
6667
import org.springframework.integration.annotation.IntegrationComponentScan;
6768
import org.springframework.integration.annotation.MessagingGateway;
69+
import org.springframework.integration.annotation.Poller;
6870
import org.springframework.integration.annotation.ServiceActivator;
6971
import org.springframework.integration.channel.DirectChannel;
72+
import org.springframework.integration.channel.QueueChannel;
7073
import org.springframework.integration.config.EnableIntegration;
7174
import org.springframework.integration.context.IntegrationContextUtils;
7275
import org.springframework.integration.context.IntegrationProperties;
@@ -93,6 +96,7 @@
9396
import org.springframework.util.ClassUtils;
9497

9598
import static org.assertj.core.api.Assertions.assertThat;
99+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
96100
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
97101
import static org.mockito.Mockito.mock;
98102
import static org.mockito.Mockito.times;
@@ -150,6 +154,9 @@ public class GatewayInterfaceTests {
150154
@Autowired
151155
private MessageChannel gatewayChannel;
152156

157+
@Autowired
158+
private PollableChannel gatewayQueueChannel;
159+
153160
@Autowired
154161
private MessageChannel errorChannel;
155162

@@ -468,19 +475,19 @@ public void testAnnotationGatewayProxyFactoryBean() {
468475
assertThat(TestUtils.getPropertyValue(this.annotationGatewayProxyFactoryBean,
469476
"defaultRequestTimeout", Expression.class).getValue()).isEqualTo(1111L);
470477
assertThat(TestUtils.getPropertyValue(this.annotationGatewayProxyFactoryBean,
471-
"defaultReplyTimeout", Expression.class).getValue()).isEqualTo(222L);
478+
"defaultReplyTimeout", Expression.class).getValue()).isEqualTo(0L);
472479

473480
Collection<MessagingGatewaySupport> messagingGateways =
474481
this.annotationGatewayProxyFactoryBean.getGateways().values();
475482
assertThat(messagingGateways.size()).isEqualTo(1);
476483

477484
MessagingGatewaySupport gateway = messagingGateways.iterator().next();
478-
assertThat(gateway.getRequestChannel()).isSameAs(this.gatewayChannel);
485+
assertThat(gateway.getRequestChannel()).isSameAs(this.gatewayQueueChannel);
479486
assertThat(gateway.getReplyChannel()).isSameAs(this.gatewayChannel);
480487
assertThat(gateway.getErrorChannel()).isSameAs(this.errorChannel);
481488
Object requestMapper = TestUtils.getPropertyValue(gateway, "requestMapper");
482489

483-
assertThat(TestUtils.getPropertyValue(requestMapper, "payloadExpression.expression")).isEqualTo("@foo");
490+
assertThat(TestUtils.getPropertyValue(requestMapper, "payloadExpression.expression")).isEqualTo("args[0]");
484491

485492
Map globalHeaderExpressions = TestUtils.getPropertyValue(requestMapper, "globalHeaderExpressions", Map.class);
486493
assertThat(globalHeaderExpressions.size()).isEqualTo(1);
@@ -489,6 +496,9 @@ public void testAnnotationGatewayProxyFactoryBean() {
489496
assertThat(barHeaderExpression).isNotNull();
490497
assertThat(barHeaderExpression).isInstanceOf(LiteralExpression.class);
491498
assertThat(((LiteralExpression) barHeaderExpression).getValue()).isEqualTo("baz");
499+
500+
assertThatExceptionOfType(MessageTimeoutException.class)
501+
.isThrownBy(() -> this.gatewayByAnnotationGPFB.foo("test"));
492502
}
493503

494504
@Test
@@ -667,6 +677,12 @@ public MessageChannel gatewayChannel() {
667677
return new DirectChannel();
668678
}
669679

680+
@Bean
681+
@BridgeTo(poller = @Poller(fixedDelay = "1000"))
682+
public MessageChannel gatewayQueueChannel() {
683+
return new QueueChannel();
684+
}
685+
670686
@Bean
671687
@BridgeTo
672688
public MessageChannel gatewayThreadChannel() {
@@ -802,13 +818,14 @@ public interface IgnoredHeaderGateway {
802818
}
803819

804820
@MessagingGateway(
805-
defaultRequestChannel = "${gateway.channel:gatewayChannel}",
821+
defaultRequestChannel = "${gateway.channel:gatewayQueueChannel}",
806822
defaultReplyChannel = "${gateway.channel:gatewayChannel}",
807-
defaultPayloadExpression = "${gateway.payload:@foo}",
823+
defaultPayloadExpression = "${gateway.payload:args[0]}",
808824
errorChannel = "${gateway.channel:errorChannel}",
809825
asyncExecutor = "${gateway.executor:exec}",
810826
defaultRequestTimeout = "${gateway.timeout:1111}",
811-
defaultReplyTimeout = "${gateway.timeout:222}",
827+
defaultReplyTimeout = "${gateway.timeout:0}",
828+
errorOnTimeout = true,
812829
defaultHeaders = {
813830
@GatewayHeader(name = "${gateway.header.name:bar}",
814831
value = "${gateway.header.value:baz}")

src/reference/antora/modules/ROOT/pages/gateway.adoc

+6
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,8 @@ If a component downstream is still running (perhaps because of an infinite loop
794794
However, if the timeout has been reached before the actual reply was produced, it could result in a 'null' return from the gateway method.
795795
You should understand that the reply message (if produced) is sent to a reply channel after the gateway method invocation might have returned, so you must be aware of that and design your flow with it in mind.
796796

797+
Also see the `errorOnTimeout` property to throw a `MessageTimeoutException` instead of returning `null`, when a timeout occurs.
798+
797799
[[downstream-component-returns-null-]]
798800
=== Downstream Component Returns 'null'
799801

@@ -838,4 +840,8 @@ At that time, the calling thread starts waiting for the reply.
838840
If the flow was completely synchronous, the reply is immediately available.
839841
For asynchronous flows, the thread waits for up to this time.
840842

843+
Starting with version 6.2, the `errorOnTimeout` property of the internal `MethodInvocationGateway` extension of the `MessagingGatewaySupport` is exposed on the `@MessagingGateway` and `GatewayEndpointSpec`.
844+
This option has exactly the same meaning as for any inbound gateway explained in the end of xref:endpoint-summary.adoc#endpoint-summary[Endpoint Summary] chapter.
845+
In other words, setting this option to `true`, would lead to the `MessageTimeoutException` being thrown from a send-and-receive gateway operation instead of returning `null` when the receive timeout is exhausted.
846+
841847
See xref:dsl/integration-flow-as-gateway.adoc[`IntegrationFlow` as Gateway] in the Java DSL chapter for options to define gateways through `IntegrationFlow`.

src/reference/antora/modules/ROOT/pages/whats-new.adoc

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ See, for example, `transformWith()`, `splitWith()` in xref:dsl.adoc#java-dsl[ Ja
3636
- A new `spring.integration.endpoints.defaultTimeout` global property has been introduced to override the default 30 seconds timeout for all the endpoints in the application.
3737
See xref:configuration/global-properties.adoc[Global Properties] for more information.
3838

39+
- The `@MessagingGateway` and `GatewayEndpointSpec` provided by the Java DSL now expose the `errorOnTimeout` property of the internal `MethodInvocationGateway` extension of the `MessagingGatewaySupport`.
40+
See xref:gateway.adoc#gateway-no-response[ Gateway Behavior When No response Arrives] for more information.
3941
[[x6.2-websockets]]
4042
=== WebSockets Changes
4143

0 commit comments

Comments
 (0)