diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/DelayerParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/DelayerParser.java index 037be313b5..192e4231d1 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/DelayerParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/DelayerParser.java @@ -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; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/handler/DelayHandler.java b/spring-integration-core/src/main/java/org/springframework/integration/handler/DelayHandler.java index 9889f5d649..756bab80ad 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/handler/DelayHandler.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/handler/DelayHandler.java @@ -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. - *

+ *

* 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. - *

+ *

* 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 headerDate.getTime() - new Date().getTime()). - * + * * @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 { - 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 only 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. - *

Default is "false". Switch this to "true" if you prefer - * fully completed tasks at the expense of a longer shutdown phase. - *

- * 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 delay > 0 schedules 'releaseMessage' task after 'delay'. + * + * @param requestMessage - the Message which may be delayed. + * @return - null 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(); - } - } } diff --git a/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-2.2.xsd b/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-2.2.xsd index 5c3ce54be6..cc92ba353a 100644 --- a/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-2.2.xsd +++ b/spring-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-2.2.xsd @@ -716,7 +716,7 @@ - + @@ -770,11 +770,11 @@ - + - Reference to a org.springframework.core.io.support.ResourcePatternResolver. + Reference to a org.springframework.core.io.support.ResourcePatternResolver. @@ -986,7 +986,7 @@ - + @@ -1085,7 +1085,7 @@ - + @@ -1138,7 +1138,7 @@ @@ -1147,7 +1147,7 @@ @@ -1156,25 +1156,25 @@ - + @@ -1268,17 +1268,28 @@ - + + + + - - - - + + + + '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. + + + + 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. @@ -1335,14 +1345,6 @@ - - - - Specify whether tasks should be able to complete on shutdown. By - default this is 'false'. - - - @@ -1358,7 +1360,7 @@ - + @@ -1372,7 +1374,7 @@ - + @@ -1635,7 +1637,7 @@ - + @@ -1895,7 +1897,7 @@ - + @@ -1934,7 +1936,7 @@ - + @@ -1951,7 +1953,7 @@ - + @@ -1965,7 +1967,7 @@ - + @@ -1979,7 +1981,7 @@ - + @@ -2021,7 +2023,7 @@ - + @@ -2065,7 +2067,7 @@ - + @@ -2107,7 +2109,7 @@ - + @@ -2142,7 +2144,7 @@ - + @@ -2185,7 +2187,7 @@ - + @@ -2213,7 +2215,7 @@ - + @@ -2279,7 +2281,7 @@ - + @@ -2987,7 +2989,7 @@ is provided, the return value is expected to match a channel name exactly. - + @@ -3037,7 +3039,7 @@ is provided, the return value is expected to match a channel name exactly. - + @@ -3188,7 +3190,7 @@ is provided, the return value is expected to match a channel name exactly. - + @@ -3604,7 +3606,7 @@ The list of component name patterns you want to track (e.g., tracked-components - + @@ -3625,7 +3627,6 @@ The list of component name patterns you want to track (e.g., tracked-components - @@ -3681,4 +3682,18 @@ endpoint itself is a Polling Consumer for a channel with a queue. + + + + + + 'id' value: + - Identifies the underlying Spring bean definition (AbstractEndpoint) + - as MessageHandler bean alias together with suffix '.handler' + + + + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerParserTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerParserTests-context.xml index cecafcd54b..1784af583a 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerParserTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerParserTests-context.xml @@ -2,11 +2,9 @@ @@ -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"/> - + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerParserTests.java index bad07009f4..e9d0b4f856 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerParserTests.java @@ -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 diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerUsageTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerUsageTests-context.xml index 5e911655f4..a24a2826cb 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerUsageTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerUsageTests-context.xml @@ -2,11 +2,9 @@ @@ -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"/> - + + scheduler="multiThreadScheduler"/> + + + + + + + + - - - + - + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerUsageTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerUsageTests.java index fd82531b9a..09cd1fed3a 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerUsageTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerUsageTests.java @@ -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("Hello")); - outputA.receive(); + assertNotNull(outputA.receive(10000)); assertTrue((System.currentTimeMillis() - start) >= 1000); } + @Test public void testDelayWithDefaultSchedulerCustomDelayHeader(){ MessageBuilder 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("1")); + inputB.send(new GenericMessage("1")); inputB.send(new GenericMessage("2")); inputB.send(new GenericMessage("3")); inputB.send(new GenericMessage("4")); inputB.send(new GenericMessage("5")); inputB.send(new GenericMessage("6")); inputB.send(new GenericMessage("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("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); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/DelayHandlerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/DelayHandlerTests.java index 3aba426bb2..a13078ca7e 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/DelayHandlerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/DelayHandlerTests.java @@ -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("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("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); diff --git a/spring-integration-groovy/src/main/resources/org/springframework/integration/groovy/config/spring-integration-groovy-2.2.xsd b/spring-integration-groovy/src/main/resources/org/springframework/integration/groovy/config/spring-integration-groovy-2.2.xsd index eb0efe1698..627fee4e21 100644 --- a/spring-integration-groovy/src/main/resources/org/springframework/integration/groovy/config/spring-integration-groovy-2.2.xsd +++ b/spring-integration-groovy/src/main/resources/org/springframework/integration/groovy/config/spring-integration-groovy-2.2.xsd @@ -7,7 +7,7 @@ elementFormDefault="qualified" attributeFormDefault="unqualified"> @@ -53,7 +53,7 @@ - + diff --git a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/DelayerHandlerRescheduleIntegrationTests-context.xml b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/DelayerHandlerRescheduleIntegrationTests-context.xml new file mode 100644 index 0000000000..78b3f2f9ed --- /dev/null +++ b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/DelayerHandlerRescheduleIntegrationTests-context.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + diff --git a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/DelayerHandlerRescheduleIntegrationTests.java b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/DelayerHandlerRescheduleIntegrationTests.java new file mode 100644 index 0000000000..702d500908 --- /dev/null +++ b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/DelayerHandlerRescheduleIntegrationTests.java @@ -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> 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> { + + public Message mapRow(ResultSet rs, int rowNum) throws SQLException { + return (Message) deserializer.convert(lobHandler.getBlobAsBytes(rs, "MESSAGE_BYTES")); + } + } + + } + +}