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:
Artem Bilan
2012-06-11 12:01:55 +03:00
committed by Gary Russell
parent 84f66e059b
commit d1d4faa151
11 changed files with 683 additions and 425 deletions

View File

@@ -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 &lt;delayer&gt; 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;
}

View File

@@ -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();
}
}
}

View File

@@ -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>

View File

@@ -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"/>

View File

@@ -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

View File

@@ -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>

View File

@@ -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);

View File

@@ -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);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"));
}
}
}
}