INT-1132: Rescheduling Support for DelayHandler
- Add support for DelayHandler to reschedule persisted Messages on startup via 'implements ApplicationListener<ContextRefreshedEvent>' as late as possible - Change DelayHandler dependency to MessageGroupStore - Add required DelayHandler.messageGroupId property - Make registered by SI-namespace TaskScheduler as default for DelayHandler - Remove 'required' from delayer xml-attribute 'default-delay' as redundant - Additional refactoring & polishing around <delayer> - Polishing delayer's Tests - Tests for 'rescheduling' - Integration test for 'rescheduling' with JdbcMS INT-1132: add 'initializingLatch' to DelayHandler INT-1132: additional polishing INT-1132: add LIFO JMS polling test-case INT-1132: Changes according to PR comments INT-1132: Changes according to PR comments 2 Polishing Remove blocking calls from tests.
This commit is contained in:
committed by
Gary Russell
parent
84f66e059b
commit
d1d4faa151
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2010 the original author or authors.
|
||||
* Copyright 2002-2012 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.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.integration.config.xml;
|
||||
|
||||
import org.springframework.integration.handler.DelayHandler;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
@@ -24,30 +25,49 @@ import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Parser for the <delayer> element.
|
||||
*
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Artem Bilan
|
||||
* @since 1.0.3
|
||||
*/
|
||||
public class DelayerParser extends AbstractConsumerEndpointParser {
|
||||
|
||||
@Override
|
||||
protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) {
|
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(
|
||||
IntegrationNamespaceUtils.BASE_PACKAGE + ".handler.DelayHandler");
|
||||
String defaultDelay = element.getAttribute("default-delay");
|
||||
if (!StringUtils.hasText(defaultDelay)) {
|
||||
parserContext.getReaderContext().error("The 'default-delay' attribute is required.", element);
|
||||
return null;
|
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(DelayHandler.class);
|
||||
|
||||
String id = element.getAttribute(ID_ATTRIBUTE);
|
||||
if (!StringUtils.hasText(id)) {
|
||||
parserContext.getReaderContext().error("The 'id' attribute is required.", element);
|
||||
}
|
||||
builder.addConstructorArgValue(defaultDelay);
|
||||
|
||||
String defaultDelay = element.getAttribute("default-delay");
|
||||
String delayHeaderName = element.getAttribute("delay-header-name");
|
||||
|
||||
boolean hasDefaultDelay = StringUtils.hasText(defaultDelay);
|
||||
boolean hasDelayHeaderName = StringUtils.hasText(delayHeaderName);
|
||||
|
||||
if (!(hasDefaultDelay | hasDelayHeaderName)) {
|
||||
parserContext.getReaderContext()
|
||||
.error("The 'default-delay' or 'delay-header-name' attributes should be provided.", element);
|
||||
}
|
||||
|
||||
builder.addConstructorArgValue(id + ".messageGroupId");
|
||||
|
||||
String scheduler = element.getAttribute("scheduler");
|
||||
if (StringUtils.hasText(scheduler)) {
|
||||
builder.addConstructorArgReference(scheduler);
|
||||
}
|
||||
|
||||
if (hasDefaultDelay) {
|
||||
builder.addPropertyValue("defaultDelay", defaultDelay);
|
||||
}
|
||||
if (hasDelayHeaderName) {
|
||||
builder.addPropertyValue("delayHeaderName", delayHeaderName);
|
||||
}
|
||||
|
||||
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "message-store");
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "delay-header-name");
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "send-timeout");
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "wait-for-tasks-to-complete-on-shutdown");
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2011 the original author or authors.
|
||||
* Copyright 2002-2012 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.
|
||||
@@ -16,31 +16,21 @@
|
||||
|
||||
package org.springframework.integration.handler;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.MessageChannel;
|
||||
import org.springframework.integration.MessageHandlingException;
|
||||
import org.springframework.integration.MessageHeaders;
|
||||
import org.springframework.integration.context.IntegrationContextUtils;
|
||||
import org.springframework.integration.context.IntegrationObjectSupport;
|
||||
import org.springframework.integration.core.MessageHandler;
|
||||
import org.springframework.integration.core.MessageProducer;
|
||||
import org.springframework.integration.core.MessagingTemplate;
|
||||
import org.springframework.integration.message.ErrorMessage;
|
||||
import org.springframework.integration.store.MessageGroup;
|
||||
import org.springframework.integration.store.MessageGroupStore;
|
||||
import org.springframework.integration.store.MessageStore;
|
||||
import org.springframework.integration.store.SimpleMessageStore;
|
||||
import org.springframework.integration.support.channel.BeanFactoryChannelResolver;
|
||||
import org.springframework.integration.support.channel.ChannelResolutionException;
|
||||
import org.springframework.integration.support.channel.ChannelResolver;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.concurrent.ExecutorConfigurationSupport;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -52,12 +42,12 @@ import org.springframework.util.Assert;
|
||||
* therefore, the calling thread does not block. The advantage of this approach
|
||||
* is that many delays can be managed concurrently, even very long delays,
|
||||
* without producing a buildup of blocked Threads.
|
||||
* <p>
|
||||
* <p/>
|
||||
* One thing to keep in mind, however, is that any active transactional context
|
||||
* will not propagate from the original sender to the eventual recipient. This
|
||||
* is a side-effect of passing the Message to the output channel after the
|
||||
* delay with a different Thread in control.
|
||||
* <p>
|
||||
* <p/>
|
||||
* When this handler's 'delayHeaderName' property is configured, that value, if
|
||||
* present on a Message, will take precedence over the handler's 'defaultDelay'
|
||||
* value. The actual header value may be a long, a String that can be parsed
|
||||
@@ -67,54 +57,49 @@ import org.springframework.util.Assert;
|
||||
* seconds from the current time). If the value is a Date, it will be
|
||||
* delayed at least until that Date occurs (i.e. the delay in that case is
|
||||
* equivalent to <code>headerDate.getTime() - new Date().getTime()</code>).
|
||||
*
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Artem Bilan
|
||||
* @since 1.0.3
|
||||
*/
|
||||
public class DelayHandler extends IntegrationObjectSupport implements MessageHandler, MessageProducer, Ordered, DisposableBean {
|
||||
public class DelayHandler extends AbstractReplyProducingMessageHandler implements ApplicationListener<ContextRefreshedEvent> {
|
||||
|
||||
private final Log logger = LogFactory.getLog(this.getClass());
|
||||
private final String messageGroupId;
|
||||
|
||||
private volatile long defaultDelay;
|
||||
|
||||
private volatile String delayHeaderName;
|
||||
|
||||
private boolean waitForTasksToCompleteOnShutdown = false;
|
||||
|
||||
private volatile MessageChannel outputChannel;
|
||||
|
||||
private volatile ChannelResolver channelResolver;
|
||||
|
||||
private volatile MessageStore messageStore;
|
||||
|
||||
private final MessagingTemplate messagingTemplate = new MessagingTemplate();
|
||||
|
||||
private volatile int order = Ordered.LOWEST_PRECEDENCE;
|
||||
private volatile MessageGroupStore messageStore;
|
||||
|
||||
private final AtomicBoolean initialized = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* Create a DelayHandler with the given default delay. The sending of Messages after
|
||||
* the delay will be handled by a scheduled thread pool with a size of 1.
|
||||
* Create a DelayHandler with the given 'messageGroupId' that is used as 'key' for {@link MessageGroup}
|
||||
* to store delayed Messages in the {@link MessageGroupStore}. The sending of Messages after
|
||||
* the delay will be handled by registered in the ApplicationContext default {@link ThreadPoolTaskScheduler}.
|
||||
*
|
||||
* @see IntegrationObjectSupport#getTaskScheduler()
|
||||
*/
|
||||
public DelayHandler(long defaultDelay) {
|
||||
this(defaultDelay, null);
|
||||
public DelayHandler(String messageGroupId) {
|
||||
Assert.notNull(messageGroupId, "'messageGroupId' must not be null");
|
||||
this.messageGroupId = messageGroupId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a DelayHandler with the given default delay. The sending of Messages
|
||||
* after the delay will be handled by the provided {@link TaskScheduler}.
|
||||
*/
|
||||
public DelayHandler(long defaultDelay, TaskScheduler taskScheduler) {
|
||||
this.defaultDelay = defaultDelay;
|
||||
this.setTaskScheduler(taskScheduler != null ? taskScheduler : new ThreadPoolTaskScheduler());
|
||||
public DelayHandler(String messageGroupId, TaskScheduler taskScheduler) {
|
||||
this(messageGroupId);
|
||||
this.setTaskScheduler(taskScheduler);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the default delay in milliseconds. If no 'delayHeaderName' property
|
||||
* has been provided, the default delay will be applied to all Messages. If
|
||||
* a delay should <emphasis>only</emphasis> be applied to Messages with a
|
||||
* header, then set this value to 0.
|
||||
* header, then set this value to 0.
|
||||
*/
|
||||
public void setDefaultDelay(long defaultDelay) {
|
||||
this.defaultDelay = defaultDelay;
|
||||
@@ -130,82 +115,57 @@ public class DelayHandler extends IntegrationObjectSupport implements MessageHan
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the {@link MessageStore} that should be used to store Messages
|
||||
* Specify the {@link MessageGroupStore} that should be used to store Messages
|
||||
* while awaiting the delay.
|
||||
*/
|
||||
public void setMessageStore(MessageStore messageStore) {
|
||||
public void setMessageStore(MessageGroupStore messageStore) {
|
||||
Assert.state(messageStore != null, "MessageStore must not be null");
|
||||
this.messageStore = messageStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the output channel for this handler. If none is provided, each
|
||||
* inbound Message must include a reply channel header.
|
||||
*/
|
||||
public void setOutputChannel(MessageChannel outputChannel) {
|
||||
this.outputChannel = outputChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the timeout for sending reply Messages.
|
||||
*/
|
||||
public void setSendTimeout(long sendTimeout) {
|
||||
this.messagingTemplate.setSendTimeout(sendTimeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to wait for scheduled tasks to complete on shutdown.
|
||||
* <p>Default is "false". Switch this to "true" if you prefer
|
||||
* fully completed tasks at the expense of a longer shutdown phase.
|
||||
* <p>
|
||||
* This property will only have an effect for TaskScheduler implementations
|
||||
* that extend from {@link ExecutorConfigurationSupport}.
|
||||
* @see ExecutorConfigurationSupport#setWaitForTasksToCompleteOnShutdown(boolean)
|
||||
*/
|
||||
public void setWaitForTasksToCompleteOnShutdown(boolean waitForJobsToCompleteOnShutdown) {
|
||||
this.waitForTasksToCompleteOnShutdown = waitForJobsToCompleteOnShutdown;
|
||||
}
|
||||
|
||||
public void setOrder(int order) {
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
public int getOrder() {
|
||||
return this.order;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getComponentType() {
|
||||
return "delayer";
|
||||
}
|
||||
|
||||
protected void onInit() throws Exception{
|
||||
if (this.getTaskScheduler() instanceof ExecutorConfigurationSupport) {
|
||||
((ExecutorConfigurationSupport) this.getTaskScheduler()).setWaitForTasksToCompleteOnShutdown(this.waitForTasksToCompleteOnShutdown);
|
||||
}
|
||||
else if (logger.isWarnEnabled()) {
|
||||
logger.warn("The 'waitForJobsToCompleteOnShutdown' property is not supported for TaskScheduler of type [" +
|
||||
this.getTaskScheduler().getClass() + "]");
|
||||
}
|
||||
@Override
|
||||
protected void onInit() {
|
||||
super.onInit();
|
||||
if (this.messageStore == null) {
|
||||
this.messageStore = new SimpleMessageStore();
|
||||
}
|
||||
if (this.getTaskScheduler() instanceof InitializingBean) {
|
||||
((InitializingBean) this.getTaskScheduler()).afterPropertiesSet();
|
||||
}
|
||||
if (this.getBeanFactory() != null){
|
||||
this.channelResolver = new BeanFactoryChannelResolver(this.getBeanFactory());
|
||||
else {
|
||||
Assert.isInstanceOf(MessageStore.class, this.messageStore);
|
||||
}
|
||||
}
|
||||
|
||||
public final void handleMessage(final Message<?> message) {
|
||||
long delay = this.determineDelayForMessage(message);
|
||||
if (delay > 0) {
|
||||
this.releaseMessageAfterDelay(message, delay);
|
||||
}
|
||||
else {
|
||||
// no delay, send directly
|
||||
this.sendMessageToReplyChannel(message);
|
||||
/**
|
||||
* Checks if 'requestMessage' wasn't delayed before
|
||||
* ({@link #releaseMessageAfterDelay} and {@link DelayedMessageWrapper}).
|
||||
* Than determine 'delay' for 'requestMessage' ({@link #determineDelayForMessage})
|
||||
* and if <code>delay > 0</code> schedules 'releaseMessage' task after 'delay'.
|
||||
*
|
||||
* @param requestMessage - the Message which may be delayed.
|
||||
* @return - <code>null</code> if 'requestMessage' is delayed,
|
||||
* otherwise - 'payload' from 'requestMessage'.
|
||||
* @see #releaseMessage
|
||||
*/
|
||||
|
||||
@Override
|
||||
protected Object handleRequestMessage(Message<?> requestMessage) {
|
||||
boolean delayed = requestMessage.getPayload() instanceof DelayedMessageWrapper;
|
||||
|
||||
if (!delayed) {
|
||||
long delay = this.determineDelayForMessage(requestMessage);
|
||||
if (delay > 0) {
|
||||
this.releaseMessageAfterDelay(requestMessage, delay);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// no delay
|
||||
Object payload = requestMessage.getPayload();
|
||||
return delayed ? ((DelayedMessageWrapper) payload).getOriginal().getPayload() : payload;
|
||||
}
|
||||
|
||||
private long determineDelayForMessage(Message<?> message) {
|
||||
@@ -231,98 +191,116 @@ public class DelayHandler extends IntegrationObjectSupport implements MessageHan
|
||||
}
|
||||
|
||||
private void releaseMessageAfterDelay(final Message<?> message, long delay) {
|
||||
Assert.state(this.messageStore != null, "MessageStore must not be null");
|
||||
final Message<?> storedMessage = this.messageStore.addMessage(message);
|
||||
Message<?> delayedMessage = message;
|
||||
|
||||
DelayedMessageWrapper messageWrapper = null;
|
||||
if (message.getPayload() instanceof DelayedMessageWrapper) {
|
||||
messageWrapper = (DelayedMessageWrapper) message.getPayload();
|
||||
}
|
||||
else {
|
||||
messageWrapper = new DelayedMessageWrapper(message);
|
||||
delayedMessage = MessageBuilder.withPayload(messageWrapper).copyHeaders(message.getHeaders()).build();
|
||||
this.messageStore.addMessageToGroup(this.messageGroupId, delayedMessage);
|
||||
}
|
||||
|
||||
final Message<?> messageToSchedule = delayedMessage;
|
||||
|
||||
this.getTaskScheduler().schedule(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
releaseMessage(storedMessage.getHeaders().getId());
|
||||
}
|
||||
catch (Exception e) {
|
||||
Exception exception = new MessageHandlingException(message, "Failed to deliver Message after delay.", e);
|
||||
MessageChannel errorChannel = resolveErrorChannelIfPossible(message);
|
||||
if (errorChannel != null) {
|
||||
ErrorMessage errorMessage = new ErrorMessage(exception);
|
||||
try {
|
||||
messagingTemplate.send(errorChannel, errorMessage);
|
||||
}
|
||||
catch (Exception e2) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("Failed to send MessagingException to error channel.", exception);
|
||||
}
|
||||
}
|
||||
releaseMessage(messageToSchedule);
|
||||
}
|
||||
}, new Date(messageWrapper.getRequestDate() + delay));
|
||||
}
|
||||
|
||||
private void releaseMessage(Message<?> message) {
|
||||
if (this.messageStore instanceof SimpleMessageStore
|
||||
|| ((MessageStore) this.messageStore).removeMessage(message.getHeaders().getId()) != null) {
|
||||
this.messageStore.removeMessageFromGroup(this.messageGroupId, message);
|
||||
this.handleMessageInternal(message);
|
||||
}
|
||||
else {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("No message in the Message Store to release: " + message +
|
||||
". Likely another instance has already released it.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for reading persisted Messages in the 'messageStore'
|
||||
* to reschedule them e.g. upon application restart.
|
||||
* The logic is based on iteration over 'messageGroup.getMessages()'
|
||||
* and schedules task about 'delay' logic.
|
||||
* This behavior is dictated by the avoidance of invocation thread overload.
|
||||
*/
|
||||
public void reschedulePersistedMessages() {
|
||||
MessageGroup messageGroup = this.messageStore.getMessageGroup(this.messageGroupId);
|
||||
for (final Message<?> message : messageGroup.getMessages()) {
|
||||
this.getTaskScheduler().schedule(new Runnable() {
|
||||
public void run() {
|
||||
long delay = determineDelayForMessage(message);
|
||||
if (delay > 0) {
|
||||
releaseMessageAfterDelay(message, delay);
|
||||
}
|
||||
else if (logger.isWarnEnabled()) {
|
||||
logger.warn("No error channel available. MessagingException will be ignored.", exception);
|
||||
else {
|
||||
releaseMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, new Date(System.currentTimeMillis() + delay));
|
||||
}, new Date());
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseMessage(UUID id) {
|
||||
Assert.state(this.messageStore != null, "MessageStore must not be null");
|
||||
Message<?> message = this.messageStore.removeMessage(id);
|
||||
Assert.notNull(message, "Message with id: " + id + " no longer exists in MessageStore.");
|
||||
this.sendMessageToReplyChannel(message);
|
||||
/**
|
||||
* Handles {@link ContextRefreshedEvent} to invoke {@link #reschedulePersistedMessages}
|
||||
* as late as possible after application context startup.
|
||||
* Also it checks {@link #initialized} to ignore
|
||||
* other {@link ContextRefreshedEvent}s which may be published
|
||||
* in the 'parent-child' contexts, e.g. in the Spring-MVC applications.
|
||||
*
|
||||
* @param event - {@link ContextRefreshedEvent} which occurs
|
||||
* after Application context is completely initialized.
|
||||
* @see #reschedulePersistedMessages
|
||||
*/
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
if (!this.initialized.getAndSet(true)) {
|
||||
this.reschedulePersistedMessages();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMessageToReplyChannel(Message<?> message) {
|
||||
MessageChannel replyChannel = this.resolveReplyChannel(message);
|
||||
this.messagingTemplate.send(replyChannel, message);
|
||||
private static final class DelayedMessageWrapper implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -4739802369074947045L;
|
||||
|
||||
private final long requestDate = System.currentTimeMillis();
|
||||
|
||||
private final Message<?> original;
|
||||
|
||||
public DelayedMessageWrapper(Message<?> original) {
|
||||
this.original = original;
|
||||
}
|
||||
|
||||
public long getRequestDate() {
|
||||
return this.requestDate;
|
||||
}
|
||||
|
||||
public Message<?> getOriginal() {
|
||||
return this.original;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
DelayedMessageWrapper that = (DelayedMessageWrapper) o;
|
||||
|
||||
return this.original.equals(that.original);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.original.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
private MessageChannel resolveReplyChannel(Message<?> message) {
|
||||
MessageChannel replyChannel = this.outputChannel;
|
||||
if (replyChannel == null) {
|
||||
replyChannel = this.resolveChannelFromHeader(message, MessageHeaders.REPLY_CHANNEL);
|
||||
}
|
||||
if (replyChannel == null) {
|
||||
throw new ChannelResolutionException(
|
||||
"unable to resolve reply channel for message: " + message);
|
||||
}
|
||||
return replyChannel;
|
||||
}
|
||||
|
||||
private MessageChannel resolveErrorChannelIfPossible(Message<?> message) {
|
||||
MessageChannel errorChannel = null;
|
||||
try {
|
||||
errorChannel = this.resolveChannelFromHeader(message, MessageHeaders.ERROR_CHANNEL);
|
||||
}
|
||||
catch (Exception e) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("Failed to resolve error channel from header.", e);
|
||||
}
|
||||
}
|
||||
if (errorChannel == null && this.channelResolver != null) {
|
||||
errorChannel = this.channelResolver.resolveChannelName(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME);
|
||||
}
|
||||
return errorChannel;
|
||||
}
|
||||
|
||||
private MessageChannel resolveChannelFromHeader(Message<?> message, String headerName) {
|
||||
MessageChannel channel = null;
|
||||
Object channelHeader = message.getHeaders().get(headerName);
|
||||
if (channelHeader != null) {
|
||||
if (channelHeader instanceof MessageChannel) {
|
||||
channel = (MessageChannel) channelHeader;
|
||||
}
|
||||
else if (channelHeader instanceof String) {
|
||||
Assert.state(this.channelResolver != null,
|
||||
"ChannelResolver is required for resolving '" + headerName + "' by name.");
|
||||
channel = this.channelResolver.resolveChannelName((String) channelHeader);
|
||||
}
|
||||
else {
|
||||
throw new ChannelResolutionException("expected a MessageChannel or String for '" +
|
||||
headerName + "', but type is [" + channelHeader.getClass() + "]");
|
||||
}
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
public void destroy() throws Exception {
|
||||
if (this.getTaskScheduler() instanceof DisposableBean) {
|
||||
((DisposableBean) this.getTaskScheduler()).destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,7 +716,7 @@
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
|
||||
|
||||
<xsd:element name="resource-inbound-channel-adapter">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
@@ -770,11 +770,11 @@
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
|
||||
|
||||
<xsd:attribute name="pattern-resolver" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Reference to a org.springframework.core.io.support.ResourcePatternResolver.
|
||||
Reference to a org.springframework.core.io.support.ResourcePatternResolver.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
@@ -986,7 +986,7 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="expressionOrInnerEndpointDefinitionAware">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
<xsd:attribute name="requires-reply" type="xsd:string" use="optional">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
@@ -1085,7 +1085,7 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="enricher-type">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -1138,7 +1138,7 @@
|
||||
<xsd:attribute name="request-timeout" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Set the timeout value for sending request messages in milliseconds.
|
||||
Set the timeout value for sending request messages in milliseconds.
|
||||
If not explicitly configured, the default is one second.
|
||||
]]>
|
||||
</xsd:documentation>
|
||||
@@ -1147,7 +1147,7 @@
|
||||
<xsd:attribute name="reply-timeout" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
Set the timeout value for receiving reply messages in milliseconds.
|
||||
Set the timeout value for receiving reply messages in milliseconds.
|
||||
If not explicitly configured, the default is one second.
|
||||
]]>
|
||||
</xsd:documentation>
|
||||
@@ -1156,25 +1156,25 @@
|
||||
<xsd:attribute name="requires-reply" use="optional" default="true">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
If you specify a 'request-channel' you can optionally set the
|
||||
'requires-reply' attribute as well. If you set this attribute to
|
||||
'true', a reply must return a non-null value.
|
||||
|
||||
If you specify a 'request-channel' you can optionally set the
|
||||
'requires-reply' attribute as well. If you set this attribute to
|
||||
'true', a reply must return a non-null value.
|
||||
|
||||
For example, you dispatch a message to the request-channel
|
||||
(backed by a 'QueueChannel'). If the reply does not return within
|
||||
the specified 'replyTimeout', then the reply message will end up
|
||||
the specified 'replyTimeout', then the reply message will end up
|
||||
being Null.
|
||||
|
||||
|
||||
By setting 'requires-reply' to 'true', a 'ReplyRequiredException'
|
||||
will be raised for null reply messages. If 'requires-reply' is set
|
||||
to false, those messages are silently dropped.
|
||||
|
||||
will be raised for null reply messages. If 'requires-reply' is set
|
||||
to false, those messages are silently dropped.
|
||||
|
||||
This attribute defaults to 'true', if not specified.]]>
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:simpleType>
|
||||
<xsd:union memberTypes="xsd:boolean xsd:string" />
|
||||
</xsd:simpleType>
|
||||
</xsd:simpleType>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="should-clone-payload">
|
||||
<xsd:annotation>
|
||||
@@ -1268,17 +1268,28 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="delayer-type">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:sequence minOccurs="0" maxOccurs="1">
|
||||
<xsd:element name="poller" type="basePollerType" />
|
||||
</xsd:sequence>
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup"/>
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
|
||||
<xsd:complexType name="delayer-type">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="poller" type="basePollerType" minOccurs="0" maxOccurs="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="default-delay" type="xsd:string" use="required">
|
||||
<xsd:attribute name="id" type="xsd:string" use="required">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
'id' value is used:
|
||||
- as the 'AbstractEndpoint' bean 'id' if this tag is defined as root element.
|
||||
- as the DelayHandler bean alias together with suffix '.handler'
|
||||
- as the 'messageGroupId' property of DelayHandler together with suffix '.messageGroupId'
|
||||
in the operations of the MessageGroupStore for scheduling delayed messages.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="default-delay" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Specify the default delay in milliseconds. This value can be set to 0
|
||||
@@ -1309,10 +1320,9 @@
|
||||
Provide a reference to the TaskScheduler instance to which
|
||||
this endpoint should
|
||||
delegate when scheduling the sending of delayed Messages. If not
|
||||
provided, the default
|
||||
will use a thread pool of
|
||||
size
|
||||
1.
|
||||
provided, the default scheduler
|
||||
registered in the ApplicationContext {ThreadPoolTaskScheduler} will be
|
||||
used.
|
||||
</xsd:documentation>
|
||||
<xsd:appinfo>
|
||||
<tool:annotation kind="ref">
|
||||
@@ -1335,14 +1345,6 @@
|
||||
</xsd:appinfo>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="wait-for-tasks-to-complete-on-shutdown" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Specify whether tasks should be able to complete on shutdown. By
|
||||
default this is 'false'.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:element name="bridge">
|
||||
@@ -1358,7 +1360,7 @@
|
||||
<xsd:any processContents="strict" namespace="##other" minOccurs="0" maxOccurs="unbounded" />
|
||||
<xsd:element ref="poller" />
|
||||
</xsd:choice>
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
|
||||
@@ -1372,7 +1374,7 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="chain-type">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -1635,7 +1637,7 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="header-enricher-type">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -1895,7 +1897,7 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="header-filter-type">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -1934,7 +1936,7 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="expressionOrInnerEndpointDefinitionAware">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -1951,7 +1953,7 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="specialized-transformer-type">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -1965,7 +1967,7 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="specialized-transformer-type">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -1979,7 +1981,7 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="map-to-object-transformer-type">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -2021,7 +2023,7 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="object-to-json-transformer-type">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -2065,7 +2067,7 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="json-to-object-transformer-type">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -2107,7 +2109,7 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="payload-serializing-transformer-type">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -2142,7 +2144,7 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="payload-deserializing-transformer-type">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -2185,7 +2187,7 @@
|
||||
<xsd:sequence minOccurs="0" maxOccurs="1">
|
||||
<xsd:element ref="poller" />
|
||||
</xsd:sequence>
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -2213,7 +2215,7 @@
|
||||
<xsd:sequence minOccurs="0" maxOccurs="1">
|
||||
<xsd:element ref="poller" />
|
||||
</xsd:sequence>
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -2279,7 +2281,7 @@
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="filter-type">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -2987,7 +2989,7 @@ is provided, the return value is expected to match a channel name exactly.
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="splitter-type">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -3037,7 +3039,7 @@ is provided, the return value is expected to match a channel name exactly.
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="aggregator-type">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -3188,7 +3190,7 @@ is provided, the return value is expected to match a channel name exactly.
|
||||
<xsd:complexType>
|
||||
<xsd:complexContent>
|
||||
<xsd:extension base="resequencer-type">
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -3604,7 +3606,7 @@ The list of component name patterns you want to track (e.g., tracked-components
|
||||
<xsd:all minOccurs="0" maxOccurs="1">
|
||||
<xsd:element name="poller" type="basePollerType" minOccurs="0" maxOccurs="1" />
|
||||
</xsd:all>
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroupWithId" />
|
||||
</xsd:extension>
|
||||
</xsd:complexContent>
|
||||
</xsd:complexType>
|
||||
@@ -3625,7 +3627,6 @@ The list of component name patterns you want to track (e.g., tracked-components
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:attributeGroup name="inputOutputChannelGroup">
|
||||
<xsd:attribute name="id" type="xsd:string" />
|
||||
<xsd:attribute name="output-channel" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:appinfo>
|
||||
@@ -3681,4 +3682,18 @@ endpoint itself is a Polling Consumer for a channel with a queue.
|
||||
</xsd:simpleType>
|
||||
</xsd:attribute>
|
||||
</xsd:attributeGroup>
|
||||
|
||||
<xsd:attributeGroup name="inputOutputChannelGroupWithId">
|
||||
<xsd:attribute name="id" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
'id' value:
|
||||
- Identifies the underlying Spring bean definition (AbstractEndpoint)
|
||||
- as MessageHandler bean alias together with suffix '.handler'
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attributeGroup ref="inputOutputChannelGroup"/>
|
||||
</xsd:attributeGroup>
|
||||
|
||||
</xsd:schema>
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
<beans:beans xmlns="http://www.springframework.org/schema/integration"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:beans="http://www.springframework.org/schema/beans"
|
||||
xmlns:task="http://www.springframework.org/schema/task"
|
||||
xmlns:p="http://www.springframework.org/schema/p"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans
|
||||
http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/task
|
||||
http://www.springframework.org/schema/task/spring-task.xsd
|
||||
http://www.springframework.org/schema/integration
|
||||
http://www.springframework.org/schema/integration/spring-integration.xsd">
|
||||
|
||||
@@ -22,8 +20,7 @@
|
||||
default-delay="1234"
|
||||
delay-header-name="foo"
|
||||
order="99"
|
||||
send-timeout="987"
|
||||
wait-for-tasks-to-complete-on-shutdown="true"/>
|
||||
send-timeout="987"/>
|
||||
|
||||
<delayer id="delayerWithCustomScheduler"
|
||||
input-channel="input"
|
||||
@@ -37,7 +34,9 @@
|
||||
default-delay="0"
|
||||
message-store="testMessageStore"/>
|
||||
|
||||
<task:scheduler id="testScheduler" pool-size="7"/>
|
||||
<beans:bean id="testScheduler" class="org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler"
|
||||
p:poolSize="7"
|
||||
p:waitForTasksToCompleteOnShutdown="true"/>
|
||||
|
||||
<beans:bean id="testMessageStore" class="org.springframework.integration.store.SimpleMessageStore"/>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2010 the original author or authors.
|
||||
* Copyright 2002-2012 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.
|
||||
@@ -17,6 +17,8 @@
|
||||
package org.springframework.integration.config.xml;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -33,6 +35,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @author Artem Bilan
|
||||
* @since 1.0.3
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@@ -57,8 +60,7 @@ public class DelayerParserTests {
|
||||
assertEquals("foo", accessor.getPropertyValue("delayHeaderName"));
|
||||
assertEquals(new Long(987), new DirectFieldAccessor(
|
||||
accessor.getPropertyValue("messagingTemplate")).getPropertyValue("sendTimeout"));
|
||||
assertEquals(Boolean.TRUE, new DirectFieldAccessor(
|
||||
accessor.getPropertyValue("taskScheduler")).getPropertyValue("waitForTasksToCompleteOnShutdown"));
|
||||
assertNull(accessor.getPropertyValue("taskScheduler"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -73,6 +75,9 @@ public class DelayerParserTests {
|
||||
assertEquals(context.getBean("output"), accessor.getPropertyValue("outputChannel"));
|
||||
assertEquals(new Long(0), accessor.getPropertyValue("defaultDelay"));
|
||||
assertEquals(context.getBean("testScheduler"), accessor.getPropertyValue("taskScheduler"));
|
||||
assertNotNull(accessor.getPropertyValue("taskScheduler"));
|
||||
assertEquals(Boolean.TRUE, new DirectFieldAccessor(
|
||||
accessor.getPropertyValue("taskScheduler")).getPropertyValue("waitForTasksToCompleteOnShutdown"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
<beans:beans xmlns="http://www.springframework.org/schema/integration"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:beans="http://www.springframework.org/schema/beans"
|
||||
xmlns:task="http://www.springframework.org/schema/task"
|
||||
xmlns:p="http://www.springframework.org/schema/p"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans
|
||||
http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/task
|
||||
http://www.springframework.org/schema/task/spring-task.xsd
|
||||
http://www.springframework.org/schema/integration
|
||||
http://www.springframework.org/schema/integration/spring-integration.xsd">
|
||||
|
||||
@@ -22,26 +20,33 @@
|
||||
default-delay="1000"
|
||||
delay-header-name="foo"
|
||||
order="99"
|
||||
send-timeout="1000"
|
||||
wait-for-tasks-to-complete-on-shutdown="true"/>
|
||||
send-timeout="1000"/>
|
||||
|
||||
<channel id="inputB"/>
|
||||
<channel id="outputB"/>
|
||||
<channel id="outputB1">
|
||||
<queue />
|
||||
</channel>
|
||||
|
||||
|
||||
<delayer id="delayerWithCustomScheduler"
|
||||
input-channel="inputB"
|
||||
output-channel="outputB"
|
||||
default-delay="1000"
|
||||
send-timeout="20000"
|
||||
scheduler="multiThreadScheduler"/>
|
||||
scheduler="multiThreadScheduler"/>
|
||||
|
||||
<chain input-channel="delayerInsideChain" output-channel="outputA">
|
||||
<transformer expression="payload.toUpperCase()"/>
|
||||
<delayer id="delayerInsideChain" default-delay="1000"/>
|
||||
<transformer expression="payload.toLowerCase()"/>
|
||||
</chain>
|
||||
|
||||
<beans:bean id="multiThreadScheduler" class="org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler"
|
||||
p:poolSize="5"
|
||||
p:waitForTasksToCompleteOnShutdown="true"/>
|
||||
|
||||
<task:scheduler id="multiThreadScheduler" pool-size="5"/>
|
||||
|
||||
<service-activator input-channel="outputB" output-channel="outputB1" method="processMessage" ref="sampleHandler"/>
|
||||
|
||||
|
||||
<beans:bean id="sampleHandler" class="org.springframework.integration.config.xml.DelayerUsageTests$SampleService"/>
|
||||
|
||||
|
||||
</beans:beans>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2010 the original author or authors.
|
||||
* Copyright 2002-2012 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.
|
||||
@@ -16,13 +16,15 @@
|
||||
|
||||
package org.springframework.integration.config.xml;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.MessageChannel;
|
||||
import org.springframework.integration.core.PollableChannel;
|
||||
import org.springframework.integration.message.GenericMessage;
|
||||
@@ -32,64 +34,81 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
/**
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Artem Bilan
|
||||
* @since 1.0.3
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration
|
||||
public class DelayerUsageTests {
|
||||
|
||||
|
||||
@Autowired @Qualifier("inputA")
|
||||
private MessageChannel inputA;
|
||||
|
||||
@Autowired
|
||||
private MessageChannel delayerInsideChain;
|
||||
|
||||
@Autowired @Qualifier("outputA")
|
||||
private PollableChannel outputA;
|
||||
|
||||
|
||||
@Autowired @Qualifier("inputB")
|
||||
private MessageChannel inputB;
|
||||
|
||||
@Autowired @Qualifier("outputB1")
|
||||
private PollableChannel outputB1;
|
||||
|
||||
|
||||
@Test
|
||||
public void testDelayWithDefaultScheduler(){
|
||||
long start = System.currentTimeMillis();
|
||||
inputA.send(new GenericMessage<String>("Hello"));
|
||||
outputA.receive();
|
||||
assertNotNull(outputA.receive(10000));
|
||||
assertTrue((System.currentTimeMillis() - start) >= 1000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDelayWithDefaultSchedulerCustomDelayHeader(){
|
||||
MessageBuilder<String> builder = MessageBuilder.withPayload("Hello");
|
||||
// set custom delay header
|
||||
builder.setHeader("foo", 2000);
|
||||
long start = System.currentTimeMillis();
|
||||
inputA.send(builder.build());
|
||||
outputA.receive();
|
||||
inputA.send(builder.build());
|
||||
assertNotNull(outputA.receive(10000));
|
||||
assertTrue((System.currentTimeMillis() - start) >= 2000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDelayWithCustomScheduler(){
|
||||
public void testDelayWithCustomScheduler(){
|
||||
long start = System.currentTimeMillis();
|
||||
inputB.send(new GenericMessage<String>("1"));
|
||||
inputB.send(new GenericMessage<String>("1"));
|
||||
inputB.send(new GenericMessage<String>("2"));
|
||||
inputB.send(new GenericMessage<String>("3"));
|
||||
inputB.send(new GenericMessage<String>("4"));
|
||||
inputB.send(new GenericMessage<String>("5"));
|
||||
inputB.send(new GenericMessage<String>("6"));
|
||||
inputB.send(new GenericMessage<String>("7"));
|
||||
outputB1.receive();
|
||||
outputB1.receive();
|
||||
outputB1.receive();
|
||||
outputB1.receive();
|
||||
outputB1.receive();
|
||||
outputB1.receive();
|
||||
outputB1.receive();
|
||||
|
||||
assertNotNull(outputB1.receive(10000));
|
||||
assertNotNull(outputB1.receive(10000));
|
||||
assertNotNull(outputB1.receive(10000));
|
||||
assertNotNull(outputB1.receive(10000));
|
||||
assertNotNull(outputB1.receive(10000));
|
||||
assertNotNull(outputB1.receive(10000));
|
||||
assertNotNull(outputB1.receive(10000));
|
||||
|
||||
// must execute under 3 seconds, since threadPool is set too 5.
|
||||
// first batch is 5 concurrent invocations on SA, then 2 more
|
||||
// elapsed time for the whole execution should be a bit over 2 seconds depending on the hardware
|
||||
assertTrue(((System.currentTimeMillis() - start) >= 1000) && ((System.currentTimeMillis() - start) < 3000));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test //INT-1132
|
||||
public void testDelayerInsideChain(){
|
||||
long start = System.currentTimeMillis();
|
||||
delayerInsideChain.send(new GenericMessage<String>("Hello"));
|
||||
Message<?> message = outputA.receive(10000);
|
||||
assertNotNull(message);
|
||||
assertTrue((System.currentTimeMillis() - start) >= 1000);
|
||||
assertEquals("hello", message.getPayload());
|
||||
}
|
||||
|
||||
public static class SampleService{
|
||||
public String processMessage(String message) throws Exception {
|
||||
Thread.sleep(500);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2010 the original author or authors.
|
||||
* Copyright 2002-2012 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,221 +19,204 @@ package org.springframework.integration.handler;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.DirectFieldAccessor;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.context.support.StaticApplicationContext;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.MessageDeliveryException;
|
||||
import org.springframework.integration.MessageHandlingException;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.channel.MessagePublishingErrorHandler;
|
||||
import org.springframework.integration.context.IntegrationContextUtils;
|
||||
import org.springframework.integration.core.MessageHandler;
|
||||
import org.springframework.integration.message.GenericMessage;
|
||||
import org.springframework.integration.store.MessageGroup;
|
||||
import org.springframework.integration.store.MessageGroupStore;
|
||||
import org.springframework.integration.store.SimpleMessageStore;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @author Artem Bilan
|
||||
* @since 1.0.3
|
||||
*/
|
||||
public class DelayHandlerTests {
|
||||
|
||||
private static final String DELAYER_MESSAGE_GROUP_ID = "testDelayer.messageGroupId";
|
||||
|
||||
private final DirectChannel input = new DirectChannel();
|
||||
|
||||
private final DirectChannel output = new DirectChannel();
|
||||
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
private ThreadPoolTaskScheduler taskScheduler;
|
||||
|
||||
private DelayHandler delayHandler;
|
||||
|
||||
private ResultHandler resultHandler = new ResultHandler();
|
||||
|
||||
@Before
|
||||
public void setChannelNames() {
|
||||
public void setup() {
|
||||
input.setBeanName("input");
|
||||
output.setBeanName("output");
|
||||
taskScheduler = new ThreadPoolTaskScheduler();
|
||||
taskScheduler.afterPropertiesSet();
|
||||
delayHandler = new DelayHandler(DELAYER_MESSAGE_GROUP_ID, taskScheduler);
|
||||
delayHandler.setOutputChannel(output);
|
||||
input.subscribe(delayHandler);
|
||||
output.subscribe(resultHandler);
|
||||
}
|
||||
|
||||
private void startDelayerHandler() {
|
||||
delayHandler.afterPropertiesSet();
|
||||
delayHandler.onApplicationEvent(new ContextRefreshedEvent(TestUtils.createTestApplicationContext()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noDelayHeaderAndDefaultDelayIsZero() throws Exception {
|
||||
DelayHandler delayHandler = new DelayHandler(0);
|
||||
ResultHandler resultHandler = new ResultHandler();
|
||||
delayHandler.setOutputChannel(output);
|
||||
delayHandler.afterPropertiesSet();
|
||||
input.subscribe(delayHandler);
|
||||
output.subscribe(resultHandler);
|
||||
this.startDelayerHandler();
|
||||
Message<?> message = MessageBuilder.withPayload("test").build();
|
||||
input.send(message);
|
||||
assertSame(message, resultHandler.lastMessage);
|
||||
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
|
||||
assertSame(Thread.currentThread(), resultHandler.lastThread);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noDelayHeaderAndDefaultDelayIsPositive() throws Exception {
|
||||
DelayHandler delayHandler = new DelayHandler(10);
|
||||
ResultHandler resultHandler = new ResultHandler();
|
||||
delayHandler.setOutputChannel(output);
|
||||
delayHandler.afterPropertiesSet();
|
||||
input.subscribe(delayHandler);
|
||||
output.subscribe(resultHandler);
|
||||
delayHandler.setDefaultDelay(10);
|
||||
this.startDelayerHandler();
|
||||
Message<?> message = MessageBuilder.withPayload("test").build();
|
||||
input.send(message);
|
||||
this.waitForLatch(1000);
|
||||
assertSame(message, resultHandler.lastMessage);
|
||||
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
|
||||
assertNotSame(Thread.currentThread(), resultHandler.lastThread);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delayHeaderAndDefaultDelayWouldTimeout() throws Exception {
|
||||
DelayHandler delayHandler = new DelayHandler(5000);
|
||||
delayHandler.setDefaultDelay(5000);
|
||||
delayHandler.setDelayHeaderName("delay");
|
||||
ResultHandler resultHandler = new ResultHandler();
|
||||
delayHandler.setOutputChannel(output);
|
||||
delayHandler.afterPropertiesSet();
|
||||
input.subscribe(delayHandler);
|
||||
output.subscribe(resultHandler);
|
||||
this.startDelayerHandler();
|
||||
Message<?> message = MessageBuilder.withPayload("test")
|
||||
.setHeader("delay", 100).build();
|
||||
input.send(message);
|
||||
this.waitForLatch(1000);
|
||||
assertSame(message, resultHandler.lastMessage);
|
||||
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
|
||||
assertNotSame(Thread.currentThread(), resultHandler.lastThread);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delayHeaderIsNegativeAndDefaultDelayWouldTimeout() throws Exception {
|
||||
DelayHandler delayHandler = new DelayHandler(5000);
|
||||
delayHandler.setDefaultDelay(5000);
|
||||
delayHandler.setDelayHeaderName("delay");
|
||||
ResultHandler resultHandler = new ResultHandler();
|
||||
delayHandler.setOutputChannel(output);
|
||||
delayHandler.afterPropertiesSet();
|
||||
input.subscribe(delayHandler);
|
||||
output.subscribe(resultHandler);
|
||||
this.startDelayerHandler();
|
||||
Message<?> message = MessageBuilder.withPayload("test")
|
||||
.setHeader("delay", -7000).build();
|
||||
input.send(message);
|
||||
this.waitForLatch(1000);
|
||||
assertSame(message, resultHandler.lastMessage);
|
||||
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
|
||||
assertSame(Thread.currentThread(), resultHandler.lastThread);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delayHeaderIsInvalidFallsBackToDefaultDelay() throws Exception {
|
||||
DelayHandler delayHandler = new DelayHandler(5);
|
||||
delayHandler.setDefaultDelay(5);
|
||||
delayHandler.setDelayHeaderName("delay");
|
||||
ResultHandler resultHandler = new ResultHandler();
|
||||
delayHandler.setOutputChannel(output);
|
||||
delayHandler.afterPropertiesSet();
|
||||
input.subscribe(delayHandler);
|
||||
output.subscribe(resultHandler);
|
||||
this.startDelayerHandler();
|
||||
Message<?> message = MessageBuilder.withPayload("test")
|
||||
.setHeader("delay", "not a number").build();
|
||||
input.send(message);
|
||||
this.waitForLatch(1000);
|
||||
assertSame(message, resultHandler.lastMessage);
|
||||
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
|
||||
assertNotSame(Thread.currentThread(), resultHandler.lastThread);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delayHeaderIsDateInTheFutureAndDefaultDelayWouldTimeout() throws Exception {
|
||||
DelayHandler delayHandler = new DelayHandler(5000);
|
||||
delayHandler.setDefaultDelay(5000);
|
||||
delayHandler.setDelayHeaderName("delay");
|
||||
ResultHandler resultHandler = new ResultHandler();
|
||||
delayHandler.setOutputChannel(output);
|
||||
delayHandler.afterPropertiesSet();
|
||||
input.subscribe(delayHandler);
|
||||
output.subscribe(resultHandler);
|
||||
this.startDelayerHandler();
|
||||
Message<?> message = MessageBuilder.withPayload("test")
|
||||
.setHeader("delay", new Date(new Date().getTime() + 150)).build();
|
||||
input.send(message);
|
||||
this.waitForLatch(3000);
|
||||
assertSame(message, resultHandler.lastMessage);
|
||||
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
|
||||
assertNotSame(Thread.currentThread(), resultHandler.lastThread);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delayHeaderIsDateInThePastAndDefaultDelayWouldTimeout() throws Exception {
|
||||
DelayHandler delayHandler = new DelayHandler(5000);
|
||||
delayHandler.setDefaultDelay(5000);
|
||||
delayHandler.setDelayHeaderName("delay");
|
||||
ResultHandler resultHandler = new ResultHandler();
|
||||
delayHandler.setOutputChannel(output);
|
||||
delayHandler.afterPropertiesSet();
|
||||
input.subscribe(delayHandler);
|
||||
output.subscribe(resultHandler);
|
||||
this.startDelayerHandler();
|
||||
Message<?> message = MessageBuilder.withPayload("test")
|
||||
.setHeader("delay", new Date(new Date().getTime() - 60 * 1000)).build();
|
||||
input.send(message);
|
||||
this.waitForLatch(3000);
|
||||
assertSame(message, resultHandler.lastMessage);
|
||||
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
|
||||
assertSame(Thread.currentThread(), resultHandler.lastThread);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delayHeaderIsNullDateAndDefaultDelayIsZero() throws Exception {
|
||||
DelayHandler delayHandler = new DelayHandler(0);
|
||||
delayHandler.setDelayHeaderName("delay");
|
||||
ResultHandler resultHandler = new ResultHandler();
|
||||
delayHandler.setOutputChannel(output);
|
||||
delayHandler.afterPropertiesSet();
|
||||
input.subscribe(delayHandler);
|
||||
output.subscribe(resultHandler);
|
||||
this.startDelayerHandler();
|
||||
Date nullDate = null;
|
||||
Message<?> message = MessageBuilder.withPayload("test")
|
||||
.setHeader("delay", nullDate).build();
|
||||
input.send(message);
|
||||
this.waitForLatch(3000);
|
||||
assertSame(message, resultHandler.lastMessage);
|
||||
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
|
||||
assertSame(Thread.currentThread(), resultHandler.lastThread);
|
||||
}
|
||||
|
||||
@Test(expected = TestTimedOutException.class)
|
||||
public void delayHeaderIsFutureDateAndTimesOut() throws Exception {
|
||||
DelayHandler delayHandler = new DelayHandler(0);
|
||||
delayHandler.setDelayHeaderName("delay");
|
||||
ResultHandler resultHandler = new ResultHandler();
|
||||
delayHandler.setOutputChannel(output);
|
||||
delayHandler.afterPropertiesSet();
|
||||
input.subscribe(delayHandler);
|
||||
output.subscribe(resultHandler);
|
||||
this.startDelayerHandler();
|
||||
Date future = new Date(new Date().getTime() + 60 * 1000);
|
||||
Message<?> message = MessageBuilder.withPayload("test")
|
||||
.setHeader("delay", future).build();
|
||||
input.send(message);
|
||||
this.waitForLatch(50);
|
||||
assertSame(message, resultHandler.lastMessage);
|
||||
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
|
||||
assertSame(Thread.currentThread(), resultHandler.lastThread);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delayHeaderIsValidStringAndDefaultDelayWouldTimeout() throws Exception {
|
||||
DelayHandler delayHandler = new DelayHandler(5000);
|
||||
delayHandler.setDefaultDelay(5000);
|
||||
delayHandler.setDelayHeaderName("delay");
|
||||
ResultHandler resultHandler = new ResultHandler();
|
||||
delayHandler.setOutputChannel(output);
|
||||
delayHandler.afterPropertiesSet();
|
||||
input.subscribe(delayHandler);
|
||||
output.subscribe(resultHandler);
|
||||
this.startDelayerHandler();
|
||||
Message<?> message = MessageBuilder.withPayload("test")
|
||||
.setHeader("delay", "20").build();
|
||||
input.send(message);
|
||||
this.waitForLatch(1000);
|
||||
assertSame(message, resultHandler.lastMessage);
|
||||
assertSame(message.getPayload(), resultHandler.lastMessage.getPayload());
|
||||
assertNotSame(Thread.currentThread(), resultHandler.lastThread);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyShutdownWithoutWaitingByDefault() throws Exception {
|
||||
DelayHandler delayHandler = new DelayHandler(5000);
|
||||
delayHandler.afterPropertiesSet();
|
||||
delayHandler.setDefaultDelay(5000);
|
||||
this.startDelayerHandler();
|
||||
delayHandler.handleMessage(new GenericMessage<String>("foo"));
|
||||
delayHandler.destroy();
|
||||
final ThreadPoolTaskScheduler taskScheduler = (ThreadPoolTaskScheduler)
|
||||
new DirectFieldAccessor(delayHandler).getPropertyValue("taskScheduler");
|
||||
taskScheduler.destroy();
|
||||
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
new Thread(new Runnable() {
|
||||
public void run() {
|
||||
@@ -252,13 +235,12 @@ public class DelayHandlerTests {
|
||||
|
||||
@Test
|
||||
public void verifyShutdownWithWait() throws Exception {
|
||||
DelayHandler delayHandler = new DelayHandler(5000);
|
||||
delayHandler.setWaitForTasksToCompleteOnShutdown(true);
|
||||
delayHandler.afterPropertiesSet();
|
||||
delayHandler.setDefaultDelay(5000);
|
||||
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
|
||||
this.startDelayerHandler();
|
||||
delayHandler.handleMessage(new GenericMessage<String>("foo"));
|
||||
delayHandler.destroy();
|
||||
final ThreadPoolTaskScheduler taskScheduler = (ThreadPoolTaskScheduler)
|
||||
new DirectFieldAccessor(delayHandler).getPropertyValue("taskScheduler");
|
||||
taskScheduler.destroy();
|
||||
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
new Thread(new Runnable() {
|
||||
public void run() {
|
||||
@@ -277,10 +259,8 @@ public class DelayHandlerTests {
|
||||
|
||||
@Test(expected = MessageDeliveryException.class)
|
||||
public void handlerThrowsExceptionWithNoDelay() throws Exception {
|
||||
DelayHandler delayHandler = new DelayHandler(0);
|
||||
delayHandler.setOutputChannel(output);
|
||||
delayHandler.afterPropertiesSet();
|
||||
input.subscribe(delayHandler);
|
||||
this.startDelayerHandler();
|
||||
output.unsubscribe(resultHandler);
|
||||
output.subscribe(new MessageHandler() {
|
||||
public void handleMessage(Message<?> message) {
|
||||
throw new UnsupportedOperationException("intentional test failure");
|
||||
@@ -292,14 +272,14 @@ public class DelayHandlerTests {
|
||||
|
||||
@Test
|
||||
public void errorChannelHeaderAndHandlerThrowsExceptionWithDelay() throws Exception {
|
||||
DelayHandler delayHandler = new DelayHandler(0);
|
||||
delayHandler.setDelayHeaderName("delay");
|
||||
delayHandler.setOutputChannel(output);
|
||||
delayHandler.afterPropertiesSet();
|
||||
DirectChannel errorChannel = new DirectChannel();
|
||||
ResultHandler resultHandler = new ResultHandler();
|
||||
MessagePublishingErrorHandler errorHandler = new MessagePublishingErrorHandler();
|
||||
errorHandler.setDefaultErrorChannel(errorChannel);
|
||||
taskScheduler.setErrorHandler(errorHandler);
|
||||
delayHandler.setDelayHeaderName("delay");
|
||||
this.startDelayerHandler();
|
||||
output.unsubscribe(resultHandler);
|
||||
errorChannel.subscribe(resultHandler);
|
||||
input.subscribe(delayHandler);
|
||||
output.subscribe(new MessageHandler() {
|
||||
public void handleMessage(Message<?> message) {
|
||||
throw new UnsupportedOperationException("intentional test failure");
|
||||
@@ -311,13 +291,10 @@ public class DelayHandlerTests {
|
||||
input.send(message);
|
||||
this.waitForLatch(1000);
|
||||
Message<?> errorMessage = resultHandler.lastMessage;
|
||||
assertEquals(MessageHandlingException.class, errorMessage.getPayload().getClass());
|
||||
MessageHandlingException exceptionPayload = (MessageHandlingException) errorMessage.getPayload();
|
||||
assertSame(message, exceptionPayload.getFailedMessage());
|
||||
assertEquals(MessageDeliveryException.class, exceptionPayload.getCause().getClass());
|
||||
MessageDeliveryException nestedException = (MessageDeliveryException) exceptionPayload.getCause();
|
||||
assertEquals(UnsupportedOperationException.class, nestedException.getCause().getClass());
|
||||
assertSame(message, nestedException.getFailedMessage());
|
||||
assertEquals(MessageDeliveryException.class, errorMessage.getPayload().getClass());
|
||||
MessageDeliveryException exceptionPayload = (MessageDeliveryException) errorMessage.getPayload();
|
||||
assertSame(message.getPayload(), exceptionPayload.getFailedMessage().getPayload());
|
||||
assertEquals(UnsupportedOperationException.class, exceptionPayload.getCause().getClass());
|
||||
assertNotSame(Thread.currentThread(), resultHandler.lastThread);
|
||||
}
|
||||
|
||||
@@ -326,16 +303,16 @@ public class DelayHandlerTests {
|
||||
String errorChannelName = "customErrorChannel";
|
||||
StaticApplicationContext context = new StaticApplicationContext();
|
||||
context.registerSingleton(errorChannelName, DirectChannel.class);
|
||||
context.registerSingleton(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME, DirectChannel.class);
|
||||
context.refresh();
|
||||
DirectChannel customErrorChannel = (DirectChannel) context.getBean(errorChannelName);
|
||||
DelayHandler delayHandler = new DelayHandler(0);
|
||||
delayHandler.setBeanFactory(context);
|
||||
MessagePublishingErrorHandler errorHandler = new MessagePublishingErrorHandler();
|
||||
errorHandler.setBeanFactory(context);
|
||||
taskScheduler.setErrorHandler(errorHandler);
|
||||
delayHandler.setDelayHeaderName("delay");
|
||||
delayHandler.setOutputChannel(output);
|
||||
delayHandler.afterPropertiesSet();
|
||||
ResultHandler resultHandler = new ResultHandler();
|
||||
this.startDelayerHandler();
|
||||
output.unsubscribe(resultHandler);
|
||||
customErrorChannel.subscribe(resultHandler);
|
||||
input.subscribe(delayHandler);
|
||||
output.subscribe(new MessageHandler() {
|
||||
public void handleMessage(Message<?> message) {
|
||||
throw new UnsupportedOperationException("intentional test failure");
|
||||
@@ -347,32 +324,26 @@ public class DelayHandlerTests {
|
||||
input.send(message);
|
||||
this.waitForLatch(1000);
|
||||
Message<?> errorMessage = resultHandler.lastMessage;
|
||||
assertEquals(MessageHandlingException.class, errorMessage.getPayload().getClass());
|
||||
MessageHandlingException exceptionPayload = (MessageHandlingException) errorMessage.getPayload();
|
||||
assertSame(message, exceptionPayload.getFailedMessage());
|
||||
assertEquals(MessageDeliveryException.class, exceptionPayload.getCause().getClass());
|
||||
MessageDeliveryException nestedException = (MessageDeliveryException) exceptionPayload.getCause();
|
||||
assertEquals(UnsupportedOperationException.class, nestedException.getCause().getClass());
|
||||
assertSame(message, nestedException.getFailedMessage());
|
||||
assertEquals(MessageDeliveryException.class, errorMessage.getPayload().getClass());
|
||||
MessageDeliveryException exceptionPayload = (MessageDeliveryException) errorMessage.getPayload();
|
||||
assertSame(message.getPayload(), exceptionPayload.getFailedMessage().getPayload());
|
||||
assertEquals(UnsupportedOperationException.class, exceptionPayload.getCause().getClass());
|
||||
assertNotSame(Thread.currentThread(), resultHandler.lastThread);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultErrorChannelAndHandlerThrowsExceptionWithDelay() throws Exception {
|
||||
StaticApplicationContext context = new StaticApplicationContext();
|
||||
context.registerSingleton(
|
||||
IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME, DirectChannel.class);
|
||||
context.registerSingleton(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME, DirectChannel.class);
|
||||
context.refresh();
|
||||
DirectChannel defaultErrorChannel = (DirectChannel) context.getBean(
|
||||
IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME);
|
||||
DelayHandler delayHandler = new DelayHandler(0);
|
||||
delayHandler.setBeanFactory(context);
|
||||
DirectChannel defaultErrorChannel = (DirectChannel) context.getBean(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME);
|
||||
MessagePublishingErrorHandler errorHandler = new MessagePublishingErrorHandler();
|
||||
errorHandler.setBeanFactory(context);
|
||||
taskScheduler.setErrorHandler(errorHandler);
|
||||
delayHandler.setDelayHeaderName("delay");
|
||||
delayHandler.setOutputChannel(output);
|
||||
delayHandler.afterPropertiesSet();
|
||||
ResultHandler resultHandler = new ResultHandler();
|
||||
this.startDelayerHandler();
|
||||
output.unsubscribe(resultHandler);
|
||||
defaultErrorChannel.subscribe(resultHandler);
|
||||
input.subscribe(delayHandler);
|
||||
output.subscribe(new MessageHandler() {
|
||||
public void handleMessage(Message<?> message) {
|
||||
throw new UnsupportedOperationException("intentional test failure");
|
||||
@@ -383,17 +354,72 @@ public class DelayHandlerTests {
|
||||
input.send(message);
|
||||
this.waitForLatch(1000);
|
||||
Message<?> errorMessage = resultHandler.lastMessage;
|
||||
assertEquals(MessageHandlingException.class, errorMessage.getPayload().getClass());
|
||||
MessageHandlingException exceptionPayload = (MessageHandlingException) errorMessage.getPayload();
|
||||
assertSame(message, exceptionPayload.getFailedMessage());
|
||||
assertEquals(MessageDeliveryException.class, exceptionPayload.getCause().getClass());
|
||||
MessageDeliveryException nestedException = (MessageDeliveryException) exceptionPayload.getCause();
|
||||
assertEquals(UnsupportedOperationException.class, nestedException.getCause().getClass());
|
||||
assertSame(message, nestedException.getFailedMessage());
|
||||
assertEquals(MessageDeliveryException.class, errorMessage.getPayload().getClass());
|
||||
MessageDeliveryException exceptionPayload = (MessageDeliveryException) errorMessage.getPayload();
|
||||
assertSame(message.getPayload(), exceptionPayload.getFailedMessage().getPayload());
|
||||
assertEquals(UnsupportedOperationException.class, exceptionPayload.getCause().getClass());
|
||||
assertNotSame(Thread.currentThread(), resultHandler.lastThread);
|
||||
}
|
||||
|
||||
|
||||
@Test //INT-1132
|
||||
public void testReschedulePersistedMessagesOnStartup() throws Exception {
|
||||
MessageGroupStore messageGroupStore = new SimpleMessageStore();
|
||||
this.delayHandler.setDefaultDelay(200);
|
||||
this.delayHandler.setMessageStore(messageGroupStore);
|
||||
this.startDelayerHandler();
|
||||
Message<?> message = MessageBuilder.withPayload("test").build();
|
||||
this.input.send(message);
|
||||
|
||||
Thread.sleep(100);
|
||||
|
||||
// emulate restart
|
||||
this.taskScheduler.destroy();
|
||||
|
||||
assertEquals(1, messageGroupStore.getMessageGroupCount());
|
||||
assertEquals(DELAYER_MESSAGE_GROUP_ID, messageGroupStore.iterator().next().getGroupId());
|
||||
assertEquals(1, messageGroupStore.messageGroupSize(DELAYER_MESSAGE_GROUP_ID));
|
||||
assertEquals(1, messageGroupStore.getMessageCountForAllMessageGroups());
|
||||
MessageGroup messageGroup = messageGroupStore.getMessageGroup(DELAYER_MESSAGE_GROUP_ID);
|
||||
Message<?> messageInStore = messageGroup.getMessages().iterator().next();
|
||||
Object payload = messageInStore.getPayload();
|
||||
assertEquals("DelayedMessageWrapper", payload.getClass().getSimpleName());
|
||||
assertEquals(message.getPayload(), TestUtils.getPropertyValue(payload, "original.payload"));
|
||||
|
||||
this.taskScheduler.afterPropertiesSet();
|
||||
this.delayHandler = new DelayHandler(DELAYER_MESSAGE_GROUP_ID, this.taskScheduler);
|
||||
this.delayHandler.setOutputChannel(output);
|
||||
this.delayHandler.setDefaultDelay(200);
|
||||
this.delayHandler.setMessageStore(messageGroupStore);
|
||||
this.startDelayerHandler();
|
||||
|
||||
long timeBeforeReceive = System.currentTimeMillis();
|
||||
assertTrue(this.latch.await(10, TimeUnit.SECONDS));
|
||||
long timeAfterReceive = System.currentTimeMillis();
|
||||
assertThat(timeAfterReceive - timeBeforeReceive, Matchers.lessThanOrEqualTo(100L));
|
||||
|
||||
assertSame(message.getPayload(), this.resultHandler.lastMessage.getPayload());
|
||||
assertNotSame(Thread.currentThread(), this.resultHandler.lastThread);
|
||||
assertEquals(1, messageGroupStore.getMessageGroupCount());
|
||||
assertEquals(0, messageGroupStore.messageGroupSize(DELAYER_MESSAGE_GROUP_ID));
|
||||
}
|
||||
|
||||
@Test //INT-1132
|
||||
// Can happen in the parent-child context e.g. Spring-MVC applications
|
||||
public void testDoubleOnApplicationEvent() throws Exception {
|
||||
this.delayHandler = Mockito.spy(this.delayHandler);
|
||||
Mockito.doAnswer(new Answer() {
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
return null;
|
||||
}
|
||||
}).when(this.delayHandler).reschedulePersistedMessages();
|
||||
|
||||
ContextRefreshedEvent contextRefreshedEvent = new ContextRefreshedEvent(TestUtils.createTestApplicationContext());
|
||||
this.delayHandler.onApplicationEvent(contextRefreshedEvent);
|
||||
this.delayHandler.onApplicationEvent(contextRefreshedEvent);
|
||||
Mockito.verify(this.delayHandler, Mockito.times(1)).reschedulePersistedMessages();
|
||||
}
|
||||
|
||||
private void waitForLatch(long timeout) {
|
||||
try {
|
||||
this.latch.await(timeout, TimeUnit.MILLISECONDS);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
elementFormDefault="qualified" attributeFormDefault="unqualified">
|
||||
|
||||
<xsd:include
|
||||
schemaLocation="http://www.springframework.org/schema/integration/scripting/spring-integration-scripting-core-2.2.xsd"
|
||||
schemaLocation="http://www.springframework.org/schema/integration/scripting/spring-integration-scripting-core-2.2.xsd"
|
||||
/>
|
||||
|
||||
<xsd:import namespace="http://www.springframework.org/schema/integration" schemaLocation="http://www.springframework.org/schema/integration/spring-integration-2.2.xsd" />
|
||||
@@ -53,7 +53,7 @@
|
||||
<xsd:element ref="integration:poller" minOccurs="0"
|
||||
maxOccurs="1" />
|
||||
</xsd:all>
|
||||
<xsd:attributeGroup ref="integration:inputOutputChannelGroup" />
|
||||
<xsd:attributeGroup ref="integration:inputOutputChannelGroupWithId" />
|
||||
<xsd:attribute name="customizer" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.springframework.org/schema/integration"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd">
|
||||
|
||||
<beans:bean id="messageStore"
|
||||
class="org.springframework.integration.jdbc.DelayerHandlerRescheduleIntegrationTests$TestJdbcMessageStore"/>
|
||||
|
||||
<channel id="output">
|
||||
<queue/>
|
||||
</channel>
|
||||
|
||||
<delayer id="#{T (org.springframework.integration.jdbc.DelayerHandlerRescheduleIntegrationTests).DELAYER_ID}"
|
||||
input-channel="input"
|
||||
output-channel="output"
|
||||
default-delay="200"
|
||||
message-store="messageStore"/>
|
||||
|
||||
</beans:beans>
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Copyright 2002-2012 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.integration.jdbc;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.springframework.context.support.AbstractApplicationContext;
|
||||
import org.springframework.context.support.ClassPathXmlApplicationContext;
|
||||
import org.springframework.core.serializer.support.DeserializingConverter;
|
||||
import org.springframework.integration.Message;
|
||||
import org.springframework.integration.MessageChannel;
|
||||
import org.springframework.integration.core.PollableChannel;
|
||||
import org.springframework.integration.store.MessageGroup;
|
||||
import org.springframework.integration.store.MessageGroupStore;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
import org.springframework.integration.util.UUIDConverter;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
||||
import org.springframework.jdbc.support.lob.DefaultLobHandler;
|
||||
import org.springframework.jdbc.support.lob.LobHandler;
|
||||
|
||||
/**
|
||||
* @author Arte Bilan
|
||||
*/
|
||||
//INT-1132
|
||||
public class DelayerHandlerRescheduleIntegrationTests {
|
||||
|
||||
public static final String DELAYER_ID = "delayerWithJdbcMS";
|
||||
|
||||
private static EmbeddedDatabase dataSource;
|
||||
|
||||
@BeforeClass
|
||||
public static void init() {
|
||||
dataSource = new EmbeddedDatabaseBuilder()
|
||||
.setType(EmbeddedDatabaseType.DERBY)
|
||||
.addScript("classpath:/org/springframework/integration/jdbc/schema-derby.sql")
|
||||
.build();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void destroy() {
|
||||
dataSource.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDelayerHandlerRescheduleWithJdbcMessageStore() throws Exception {
|
||||
AbstractApplicationContext context = new ClassPathXmlApplicationContext("DelayerHandlerRescheduleIntegrationTests-context.xml", this.getClass());
|
||||
MessageChannel input = context.getBean("input", MessageChannel.class);
|
||||
MessageGroupStore messageStore = context.getBean("messageStore", MessageGroupStore.class);
|
||||
|
||||
assertEquals(0, messageStore.getMessageGroupCount());
|
||||
input.send(MessageBuilder.withPayload("test1").build());
|
||||
input.send(MessageBuilder.withPayload("test2").build());
|
||||
|
||||
// Emulate restart and check DB state before next start
|
||||
context.destroy();
|
||||
|
||||
Thread.sleep(100);
|
||||
|
||||
try {
|
||||
context.getBean("input", MessageChannel.class);
|
||||
fail("IllegalStateException expected");
|
||||
}
|
||||
catch (Exception e) {
|
||||
assertTrue(e instanceof IllegalStateException);
|
||||
assertTrue(e.getMessage().contains("BeanFactory not initialized or already closed - call 'refresh'"));
|
||||
}
|
||||
|
||||
String delayerMessageGroupId = UUIDConverter.getUUID(DELAYER_ID + ".messageGroupId").toString();
|
||||
|
||||
assertEquals(1, messageStore.getMessageGroupCount());
|
||||
assertEquals(delayerMessageGroupId, messageStore.iterator().next().getGroupId());
|
||||
assertEquals(2, messageStore.messageGroupSize(delayerMessageGroupId));
|
||||
assertEquals(2, messageStore.getMessageCountForAllMessageGroups());
|
||||
MessageGroup messageGroup = messageStore.getMessageGroup(delayerMessageGroupId);
|
||||
Message<?> messageInStore = messageGroup.getMessages().iterator().next();
|
||||
Object payload = messageInStore.getPayload();
|
||||
assertEquals("DelayedMessageWrapper", payload.getClass().getSimpleName());
|
||||
assertEquals("test1", TestUtils.getPropertyValue(payload, "original.payload"));
|
||||
|
||||
context.refresh();
|
||||
|
||||
PollableChannel output = context.getBean("output", PollableChannel.class);
|
||||
|
||||
long timeBeforeReceive = System.currentTimeMillis();
|
||||
Message<?> message = output.receive(10000);
|
||||
assertNotNull(message);
|
||||
long timeAfterReceive = System.currentTimeMillis();
|
||||
assertThat(timeAfterReceive - timeBeforeReceive, Matchers.lessThanOrEqualTo(100L));
|
||||
|
||||
Object payload1 = message.getPayload();
|
||||
|
||||
message = output.receive(10000);
|
||||
assertNotNull(message);
|
||||
Object payload2 = message.getPayload();
|
||||
assertNotSame(payload1, payload2);
|
||||
|
||||
assertEquals(1, messageStore.getMessageGroupCount());
|
||||
assertEquals(0, messageStore.messageGroupSize(delayerMessageGroupId));
|
||||
|
||||
}
|
||||
|
||||
private static class TestJdbcMessageStore extends JdbcMessageStore {
|
||||
|
||||
private volatile LobHandler lobHandler = new DefaultLobHandler();
|
||||
|
||||
private volatile MessageMapper mapper = new MessageMapper();
|
||||
|
||||
private volatile DeserializingConverter deserializer = new DeserializingConverter();
|
||||
|
||||
private TestJdbcMessageStore() {
|
||||
super();
|
||||
this.setDataSource(dataSource);
|
||||
}
|
||||
|
||||
|
||||
// LIFO polling
|
||||
@Override
|
||||
protected Message<?> doPollForMessage(String groupIdKey) {
|
||||
List<Message<?>> messages = this.getJdbcOperations()
|
||||
.query("SELECT INT_MESSAGE.MESSAGE_ID, INT_MESSAGE.MESSAGE_BYTES from INT_MESSAGE " +
|
||||
"where INT_MESSAGE.MESSAGE_ID = " +
|
||||
"(SELECT max(MESSAGE_ID) from INT_MESSAGE where CREATED_DATE = " +
|
||||
"(SELECT max(CREATED_DATE) from INT_MESSAGE, INT_GROUP_TO_MESSAGE " +
|
||||
"where INT_MESSAGE.MESSAGE_ID = INT_GROUP_TO_MESSAGE.MESSAGE_ID " +
|
||||
"and INT_GROUP_TO_MESSAGE.GROUP_KEY = ?))", new Object[]{groupIdKey}, mapper);
|
||||
if (messages.size() > 0) {
|
||||
System.out.println(messages.get(0));
|
||||
return messages.get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private class MessageMapper implements RowMapper<Message<?>> {
|
||||
|
||||
public Message<?> mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
return (Message<?>) deserializer.convert(lobHandler.getBlobAsBytes(rs, "MESSAGE_BYTES"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user