INT-3781: Configure HTTP GW Timeout Status Code

JIRA: https://jira.spring.io/browse/INT-3781

- Add mechanisms to set status code on an inbound gateway timeout.
- Send a message to the error channel if configured.

Polishing - Add failedMessage to MTE

Fix Schema Docs; Timeout Detection on Error Flow

Add Zookeeper Leadership Logs

Fix typo in the `SmartLifecycleRoleController`
This commit is contained in:
Gary Russell
2015-07-24 13:02:58 -04:00
committed by Artem Bilan
parent 178c86faa4
commit 9944e54ce5
9 changed files with 256 additions and 32 deletions

View File

@@ -16,6 +16,7 @@
package org.springframework.integration.gateway;
import org.springframework.integration.MessageTimeoutException;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.endpoint.EventDrivenConsumer;
@@ -50,6 +51,16 @@ public abstract class MessagingGatewaySupport extends AbstractEndpoint implement
private static final long DEFAULT_TIMEOUT = 1000L;
private final SimpleMessageConverter messageConverter = new SimpleMessageConverter();
private final MessagingTemplate messagingTemplate;
private final HistoryWritingMessagePostProcessor historyWritingPostProcessor =
new HistoryWritingMessagePostProcessor();
private final Object replyMessageCorrelatorMonitor = new Object();
private final boolean errorOnTimeout;
private volatile MessageChannel requestChannel;
@@ -68,26 +79,34 @@ public abstract class MessagingGatewaySupport extends AbstractEndpoint implement
@SuppressWarnings("rawtypes")
private volatile InboundMessageMapper requestMapper = new DefaultRequestMapper();
private final SimpleMessageConverter messageConverter = new SimpleMessageConverter();
private final MessagingTemplate messagingTemplate;
private final HistoryWritingMessagePostProcessor historyWritingPostProcessor =
new HistoryWritingMessagePostProcessor();
private volatile boolean initialized;
private volatile AbstractEndpoint replyMessageCorrelator;
private final Object replyMessageCorrelatorMonitor = new Object();
/**
* Construct an instance that will return null if no reply is received.
*/
public MessagingGatewaySupport() {
this(false);
}
/**
* If errorOnTimeout is true, construct an instance that will send an
* {@link ErrorMessage} with a {@link MessageTimeoutException} payload to the error
* channel if a reply is expected but none is received. If no error channel is
* configured, the {@link MessageTimeoutException} will be thrown.
*
* @param errorOnTimeout true to create the error message.
* @since 4.2
*/
public MessagingGatewaySupport(boolean errorOnTimeout) {
MessagingTemplate template = new MessagingTemplate();
template.setMessageConverter(this.messageConverter);
template.setSendTimeout(DEFAULT_TIMEOUT);
template.setReceiveTimeout(this.replyTimeout);
this.messagingTemplate = template;
this.errorOnTimeout = errorOnTimeout;
}
@@ -333,6 +352,14 @@ public abstract class MessagingGatewaySupport extends AbstractEndpoint implement
error = ((ErrorMessage) reply).getPayload();
}
}
if (reply == null && this.errorOnTimeout) {
if (object instanceof Message) {
error = new MessageTimeoutException((Message<?>) object, "No reply received within timeout");
}
else {
error = new MessageTimeoutException("No reply received within timeout");
}
}
}
catch (Exception e) {
if (logger.isDebugEnabled()) {
@@ -363,6 +390,15 @@ public abstract class MessagingGatewaySupport extends AbstractEndpoint implement
if (errorFlowReply != null && errorFlowReply.getPayload() instanceof Throwable) {
this.rethrow((Throwable) errorFlowReply.getPayload(), "error flow returned an Error Message");
}
if (errorFlowReply == null && this.errorOnTimeout) {
if (object instanceof Message) {
throw new MessageTimeoutException((Message<?>) object,
"No reply received from error channel within timeout");
}
else {
throw new MessageTimeoutException("No reply received from error channel within timeout");
}
}
return errorFlowReply;
}
else { // no errorChannel so we'll propagate

View File

@@ -141,15 +141,23 @@ public class SmartLifecycleRoleController implements ApplicationListener<Abstrac
}
});
if (logger.isDebugEnabled()) {
logger.debug("Zookeeper leadership granted: Starting: " + lifecycles);
}
for (SmartLifecycle lifecycle : lifecycles) {
try {
lifecycle.start();
}
catch (Exception e) {
logger.error("Failed to start " + lifecycle + " in role " + role);
logger.error("Failed to start " + lifecycle + " in role " + role, e);
}
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Zookeeper leadership granted: Nothing to do");
}
}
}
/**
@@ -172,15 +180,23 @@ public class SmartLifecycleRoleController implements ApplicationListener<Abstrac
}
});
if (logger.isDebugEnabled()) {
logger.debug("Zookeeper leadership revoked: Stopping: " + lifecycles);
}
for (SmartLifecycle lifecycle : lifecycles) {
try {
lifecycle.stop();
}
catch (Exception e) {
logger.error("Failed to stop " + lifecycle + " in role " + role);
logger.error("Failed to stop " + lifecycle + " in role " + role, e);
}
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Zookeeper leadership revoked: Nothing to do");
}
}
}
private void addLazyLifecycles() {

View File

@@ -195,6 +195,10 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse
BeanDefinition statusCodeExpressionDef =
IntegrationNamespaceUtils.createExpressionDefIfAttributeDefined("status-code-expression", element);
if (statusCodeExpressionDef == null) {
statusCodeExpressionDef = IntegrationNamespaceUtils
.createExpressionDefIfAttributeDefined("reply-timeout-status-code-expression", element);
}
if (statusCodeExpressionDef != null) {
builder.addPropertyValue("statusCodeExpression", statusCodeExpressionDef);
}

View File

@@ -50,6 +50,7 @@ import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConvert
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.integration.MessageTimeoutException;
import org.springframework.integration.context.OrderlyShutdownCapable;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.gateway.MessagingGatewaySupport;
@@ -122,6 +123,8 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
private final List<HttpMessageConverter<?>> defaultMessageConverters = new ArrayList<HttpMessageConverter<?>>();
private final boolean expectReply;
private volatile List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
private volatile RequestMapping requestMapping = new RequestMapping();
@@ -136,8 +139,6 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
private volatile HeaderMapper<HttpHeaders> headerMapper = DefaultHttpHeaderMapper.inboundMapper();
private final boolean expectReply;
private volatile boolean extractReplyPayload = true;
private volatile MultipartResolver multipartResolver;
@@ -159,6 +160,7 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
}
public HttpRequestHandlingEndpointSupport(boolean expectReply) {
super(expectReply);
this.expectReply = expectReply;
this.defaultMessageConverters.add(new MultipartAwareFormHttpMessageConverter());
this.defaultMessageConverters.add(new ByteArrayHttpMessageConverter());
@@ -331,7 +333,8 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
/**
* Specify the {@link Expression} to resolve a status code for Response
* to override the default '200 OK'.
* <p> The {@link #statusCodeExpression} is applied only for the one-way {@code <http:inbound-channel-adapter/>}.
* <p> The {@link #statusCodeExpression} is applied only for the one-way {@code <http:inbound-channel-adapter/>}
* or when no reply (timeout) is received for a gateway.
* The {@code <http:inbound-gateway/>} resolves an {@link HttpStatus} from the
* {@link org.springframework.integration.http.HttpHeaders#STATUS_CODE} reply {@link Message} header.
* @param statusCodeExpression The status code Expression.
@@ -378,11 +381,6 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
}
this.validateSupportedMethods();
if (this.expectReply && this.statusCodeExpression != null) {
logger.warn("The 'statusCodeExpression' is ignored when " +
"this component is configured as request/reply gateway");
}
if (this.statusCodeExpression != null) {
this.evaluationContext = createEvaluationContext();
}
@@ -505,7 +503,23 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
Message<?> reply = null;
if (this.expectReply) {
reply = this.sendAndReceiveMessage(message);
try {
reply = this.sendAndReceiveMessage(message);
}
catch (MessageTimeoutException e) {
if (this.statusCodeExpression != null) {
reply = getMessageBuilderFactory().withPayload(e.getMessage())
.setHeader(org.springframework.integration.http.HttpHeaders.STATUS_CODE,
evaluateHttpStatus())
.build();
}
else {
reply = getMessageBuilderFactory().withPayload(e.getMessage())
.setHeader(org.springframework.integration.http.HttpHeaders.STATUS_CODE,
HttpStatus.INTERNAL_SERVER_ERROR)
.build();
}
}
}
else {
this.send(message);
@@ -552,17 +566,22 @@ public abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewa
protected void setStatusCodeIfNeeded(ServletServerHttpResponse response) {
if (this.statusCodeExpression != null) {
if (this.evaluationContext == null) {
this.evaluationContext = createEvaluationContext();
}
Object value = this.statusCodeExpression.getValue(this.evaluationContext);
HttpStatus httpStatus = buildHttpStatus(value);
HttpStatus httpStatus = evaluateHttpStatus();
if (httpStatus != null) {
response.setStatusCode(httpStatus);
}
}
}
private HttpStatus evaluateHttpStatus() {
if (this.evaluationContext == null) {
this.evaluationContext = createEvaluationContext();
}
Object value = this.statusCodeExpression.getValue(this.evaluationContext);
HttpStatus httpStatus = buildHttpStatus(value);
return httpStatus;
}
/**
* Prepares an instance of {@link ServletServerHttpRequest} from the raw
* {@link HttpServletRequest}. Also converts the request into a multipart request to

View File

@@ -63,12 +63,12 @@
<xsd:annotation>
<xsd:documentation>
A SpEL expression that resolves to an 'HttpStatus' code when rendering a response.
The expression must return the object which can be converted to a
The expression must return an object which can be converted to a
'org.springframework.http.HttpStatus' enum value.
The 'evaluationContext' has a 'BeanResolver' but no variables, so the usage of this attribute
is somewhat limited.
An example might be to resolve, at runtime, some scoped Bean that returns an
'HttpStatus' value.
'HttpStatus' value, or use a literal expression e.g. "201".
By default 'status-code-expression' is null, meaning that the default '200 OK' response status
will be returned.
The 'http:inbound-gateway' resolves the 'status code' from the 'http_statusCode' header of the reply
@@ -203,6 +203,25 @@
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="reply-timeout-status-code-expression" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
A SpEL expression that resolves to an 'HttpStatus' code when rendering a response after
a 'reply-timeout'.
The expression must return an object which can be converted to a
'org.springframework.http.HttpStatus' enum value.
The 'evaluationContext' has a 'BeanResolver' but no variables, so the usage of this attribute
is somewhat limited.
An example might be to resolve, at runtime, some scoped Bean that returns an
'HttpStatus' value, or use a literal expression e.g. "504".
By default 'status-code-expression' is null, meaning that the default
'500 Internal Server Error' response status will be returned after a timeout.
When a timeout is not encountered,
the 'http:inbound-gateway' resolves the 'status code' from the 'http_statusCode' header of the reply
Message.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -19,6 +19,7 @@ package org.springframework.integration.http.inbound;
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.mockito.Mockito.mock;
import java.io.IOException;
@@ -26,27 +27,32 @@ import java.io.PrintWriter;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.messaging.Message;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
import org.springframework.integration.http.AbstractHttpInboundTests;
import org.springframework.integration.http.HttpHeaders;
import org.springframework.integration.http.converter.SerializingHttpMessageConverter;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.util.LinkedMultiValueMap;
@@ -296,6 +302,92 @@ public class HttpRequestHandlingMessagingGatewayTests extends AbstractHttpInboun
assertEquals("text/plain", contentTypes.get(0));
}
@Test
public void timeoutDefault() throws Exception {
QueueChannel requestChannel = new QueueChannel();
HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(true);
gateway.setBeanFactory(mock(BeanFactory.class));
gateway.setRequestChannel(requestChannel);
gateway.setReplyTimeout(0);
gateway.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
MockHttpServletResponse response = new MockHttpServletResponse();
gateway.handleRequest(request, response);
Message<?> message = requestChannel.receive(0);
assertNotNull(message);
assertEquals(500, response.getStatus());
}
@Test
public void timeoutStatusExpression() throws Exception {
QueueChannel requestChannel = new QueueChannel();
HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(true);
gateway.setBeanFactory(mock(BeanFactory.class));
gateway.setRequestChannel(requestChannel);
gateway.setReplyTimeout(0);
gateway.setStatusCodeExpression(new LiteralExpression("501"));
gateway.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
MockHttpServletResponse response = new MockHttpServletResponse();
gateway.handleRequest(request, response);
Message<?> message = requestChannel.receive(0);
assertNotNull(message);
assertEquals(501, response.getStatus());
}
@Test
public void timeoutErrorFlow() throws Exception {
QueueChannel requestChannel = new QueueChannel();
HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(true);
gateway.setBeanFactory(mock(BeanFactory.class));
gateway.setRequestChannel(requestChannel);
gateway.setReplyTimeout(0);
DirectChannel errorChannel = new DirectChannel();
errorChannel.subscribe(new AbstractReplyProducingMessageHandler() {
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
return new GenericMessage<String>("foo",
Collections.<String, Object> singletonMap(HttpHeaders.STATUS_CODE, HttpStatus.GATEWAY_TIMEOUT));
}
});
gateway.setErrorChannel(errorChannel);
gateway.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
MockHttpServletResponse response = new MockHttpServletResponse();
gateway.handleRequest(request, response);
Message<?> message = requestChannel.receive(0);
assertNotNull(message);
assertEquals(504, response.getStatus());
}
@Test
public void timeoutErrorFlowTimeout() throws Exception {
QueueChannel requestChannel = new QueueChannel();
HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(true);
gateway.setBeanFactory(mock(BeanFactory.class));
gateway.setRequestChannel(requestChannel);
gateway.setReplyTimeout(0);
QueueChannel errorChannel = new QueueChannel();
gateway.setErrorChannel(errorChannel);
gateway.setStatusCodeExpression(new LiteralExpression("501"));
gateway.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
MockHttpServletResponse response = new MockHttpServletResponse();
gateway.handleRequest(request, response);
Message<?> message = requestChannel.receive(0);
assertNotNull(message);
assertEquals(501, response.getStatus());
assertThat(response.getContentAsString(), Matchers.containsString("from error channel"));
}
private class ContentTypeCheckingMockHttpServletResponse extends MockHttpServletResponse {
private final List<String> contentTypeList = new ArrayList<String>();

View File

@@ -6,3 +6,4 @@ log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss.SSS} %-5p [%t][%c] %m
log4j.category.org.springframework.integration=WARN
log4j.category.org.springframework.integration.zookeeper=INFO
log4j.category.org.springframework.integration.support.SmartLifecycleRoleController=DEBUG

View File

@@ -267,7 +267,7 @@ This allows you to get the `Message` into the flow as early as possibly, e.g.:
For more information regarding _Handler Mappings_, please see: http://static.springsource.org/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-handlermapping[Handler Mappings].
[[cors]]
[[http-cors]]
==== Cross-Origin Resource Sharing (CORS) Support
Starting with _version 4.2_ the `<http:inbound-channel-adapter>` and `<http:inbound-gateway>` can be configured with
@@ -308,12 +308,13 @@ Default value is 1800 seconds, or 30 minutes.
The CORS Java Configuration is represented by the `org.springframework.integration.http.inbound.CrossOrigin` class,
instances of which can be injected to the `HttpRequestHandlingEndpointSupport` beans.
[[http-response-statuscode]]
==== Response StatusCode
Starting with _version 4.1_ the `<http:inbound-channel-adapter>` can be configured with a `status-code-expression` to override the default `200 OK` status.
The expression must return an object which can be converted to an `org.springframework.http.HttpStatus` enum value.
The `evaluationContext` has a `BeanResolver` but no variables, so the usage of this attribute is somewhat limited.
An example might be to resolve, at runtime, some scoped Bean that returns a status code value but, most likely, it will be set to a fixed value such as `status-code=expression="'204'"` (No Content), or `status-code-expression="T(org.springframework.http.HttpStatus).NO_CONTENT"`.
An example might be to resolve, at runtime, some scoped Bean that returns a status code value but, most likely, it will be set to a fixed value such as `status-code=expression="204"` (No Content), or `status-code-expression="T(org.springframework.http.HttpStatus).NO_CONTENT"`.
By default, `status-code-expression` is null meaning that the normal '200 OK' response status will be returned.
[source,xml]
----
@@ -325,6 +326,33 @@ By default, `status-code-expression` is null meaning that the normal '200 OK' re
----
The `<http:inbound-gateway>` resolves the 'status code' from the `http_statusCode` header of the reply Message.
Starting with _version 4.2_, the default response status code when no reply is received within the `reply-timeout`
is `500 Internal Server Error`.
There are two ways to modify this behavior:
- add a `reply-timeout-status-code-expression` - this has the same semantics as the `status-code-expression` on the
inbound adapter.
- Add an `error-channel` and return an appropriate message with an http status code header, such as...
[source, xml]
----
<int:chain input-channel="errors">
<int:header-enricher>
<int:header name="http_statusCode" value="504" />
</int:header-enricher>
<int:transformer expression="payload.message" />
</int:chain>
----
The payload of the `ErrorMessage` is a `MessageTimeoutException`; it must be transformed to something that can be
converted by the gateway, such as a `String`; a good candidate is the exception's message property, which is the
value used when using the expression technique.
If the error flow times out after a main flow timeout, `500 Internal Server Error` is returned, or the
`reply-timeout-status-code-expression` is evaluated, if present.
NOTE: previously, the default status code for a timeout was `200 OK`; to restore that behavior, set
`reply-timeout-status-code-expression="200"`.
==== URI Template Variables and Expressions

View File

@@ -202,10 +202,19 @@ See <<xml-xpath-splitting>> for more information.
[[x4.2-http-changes]]
==== HTTP Changes
===== CORS
The HTTP Inbound Endpoints (`<int-http:inbound-channel-adapter>` and `<int-http:inbound-gateway>`) now allow the
configuration of _Cross-Origin Resource Sharing (CORS)_.
See <<cors>> for more information.
See <<http-cors>> for more information.
===== Inbound Gateway Timeout
The HTTP inbound gateway can be configured as to what status code to return when a request times out.
The default is now `500 Internal Server Error` instead of `200 OK`.
See <<http-response-statuscode>> for more information.
[[x4.2-file-filter]]
==== Persistent File List Filter Changes