INT-2595: Add Time-Based ReleaseStrategy Option

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

* Add `group-timeout` and `group-timeout-expression` to the Correlation Endpoint
* Add logic to the `AbstractCorrelatingMessageHandler` to schedule group for `forceComplete`,
when the target `ReleaseStrategy` returns `false`

INT-2595: Polishing according PR comments

INT-2595: Expose `lock-registry` and further docs

INT-2595 Doc and Test Polishing

INT-2595: Fix typos

INT-2595 More Minor Doc Polish
This commit is contained in:
Artem Bilan
2014-04-02 14:03:18 +03:00
committed by Gary Russell
parent 882da62ba0
commit eb0d1ddc84
9 changed files with 388 additions and 21 deletions

View File

@@ -16,7 +16,12 @@ package org.springframework.integration.aggregator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.locks.Lock;
import org.apache.commons.logging.Log;
@@ -25,10 +30,14 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.channel.NullChannel;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.expression.IntegrationEvaluationContextAware;
import org.springframework.integration.handler.AbstractMessageHandler;
import org.springframework.integration.store.MessageGroup;
import org.springframework.integration.store.MessageGroupStore;
@@ -43,6 +52,7 @@ import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.core.DestinationResolutionException;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
@@ -70,12 +80,15 @@ import org.springframework.util.StringUtils;
* @author Artem Bilan
* @since 2.0
*/
public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageHandler implements MessageProducer {
public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageHandler
implements MessageProducer, DisposableBean, IntegrationEvaluationContextAware {
private static final Log logger = LogFactory.getLog(AbstractCorrelatingMessageHandler.class);
public static final long DEFAULT_SEND_TIMEOUT = 1000L;
private final Map<UUID, ScheduledFuture<?>> expireGroupScheduledFutures = new HashMap<UUID, ScheduledFuture<?>>();
protected volatile MessageGroupStore messageStore;
private final MessageGroupProcessor outputProcessor;
@@ -106,6 +119,10 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageH
private volatile boolean releasePartialSequences;
private volatile Expression groupTimeoutExpression;
private EvaluationContext evaluationContext;
public AbstractCorrelatingMessageHandler(MessageGroupProcessor processor, MessageGroupStore store,
CorrelationStrategy correlationStrategy, ReleaseStrategy releaseStrategy) {
Assert.notNull(processor);
@@ -166,6 +183,20 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageH
this.outputChannelName = outputChannelName;
}
public void setGroupTimeoutExpression(Expression groupTimeoutExpression) {
this.groupTimeoutExpression = groupTimeoutExpression;
}
@Override
public void setIntegrationEvaluationContext(EvaluationContext evaluationContext) {
this.evaluationContext = evaluationContext;
}
@Override
public void setTaskScheduler(TaskScheduler taskScheduler) {
super.setTaskScheduler(taskScheduler);
}
@Override
protected void onInit() throws Exception {
super.onInit();
@@ -277,11 +308,18 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageH
logger.debug("Handling message with correlationKey [" + correlationKey + "]: " + message);
}
// TODO: INT-1117 - make the lock global?
Lock lock = this.lockRegistry.obtain(UUIDConverter.getUUID(correlationKey).toString());
UUID groupIdUuid = UUIDConverter.getUUID(correlationKey);
Lock lock = this.lockRegistry.obtain(groupIdUuid.toString());
lock.lockInterruptibly();
try {
ScheduledFuture<?> scheduledFuture = this.expireGroupScheduledFutures.remove(groupIdUuid);
if (scheduledFuture != null) {
boolean canceled = scheduledFuture.cancel(true);
if (canceled && logger.isDebugEnabled()) {
logger.debug("Cancel 'forceComplete' scheduling for MessageGroup with Correlation Key [ " + correlationKey + "].");
}
}
MessageGroup messageGroup = messageStore.getMessageGroup(correlationKey);
if (this.sequenceAware){
messageGroup = new SequenceAwareMessageGroup(messageGroup);
@@ -304,6 +342,31 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageH
this.afterRelease(messageGroup, completedMessages);
}
}
else {
Long groupTimeout = this.obtainGroupTimeout(messageGroup);
if (groupTimeout != null && groupTimeout >= 0) {
if (groupTimeout > 0) {
final MessageGroup messageGroupToSchedule = messageGroup;
scheduledFuture = this.getTaskScheduler()
.schedule(new Runnable() {
@Override
public void run() {
AbstractCorrelatingMessageHandler.this.forceComplete(messageGroupToSchedule);
}
}, new Date(System.currentTimeMillis() + groupTimeout));
if (logger.isDebugEnabled()) {
logger.debug("Schedule MessageGroup [ " + messageGroup + "] to 'forceComplete'.");
}
this.expireGroupScheduledFutures.put(groupIdUuid, scheduledFuture);
}
else {
this.forceComplete(messageGroup);
}
}
}
}
else {
discardChannel.send(message);
@@ -330,6 +393,13 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageH
try {
lock.lockInterruptibly();
try {
ScheduledFuture<?> scheduledFuture = this.expireGroupScheduledFutures.remove(UUIDConverter.getUUID(correlationKey));
if (scheduledFuture != null) {
boolean canceled = scheduledFuture.cancel(false);
if (canceled && logger.isDebugEnabled()) {
logger.debug("Cancel 'forceComplete' scheduling for MessageGroup [ " + group + "].");
}
}
MessageGroup groupNow = group;
/*
* If the group argument is not already complete,
@@ -392,7 +462,7 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageH
}
catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new MessagingException("Thread was interrupted while trying to obtain lock");
logger.debug("Thread was interrupted while trying to obtain lock");
}
}
@@ -405,7 +475,7 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageH
List<Message<?>> sorted = new ArrayList<Message<?>>(partialSequence);
Collections.sort(sorted, new SequenceNumberComparator());
Message<?> lastReleasedMessage = sorted.get(partialSequence.size()-1);
Message<?> lastReleasedMessage = sorted.get(partialSequence.size() - 1);
return new IntegrationMessageHeaderAccessor(lastReleasedMessage).getSequenceNumber();
}
@@ -512,6 +582,18 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageH
return false;
}
private Long obtainGroupTimeout(MessageGroup group) {
return this.groupTimeoutExpression != null
? this.groupTimeoutExpression.getValue(this.evaluationContext, group, Long.class) : null;
}
@Override
public void destroy() throws Exception {
for (ScheduledFuture<?> future : expireGroupScheduledFutures.values()) {
future.cancel(true);
}
}
private static class SequenceAwareMessageGroup extends SimpleMessageGroup {
public SequenceAwareMessageGroup(MessageGroup messageGroup) {

View File

@@ -15,6 +15,7 @@ package org.springframework.integration.config.xml;
import org.w3c.dom.Element;
import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
@@ -66,10 +67,17 @@ public abstract class AbstractCorrelatingMessageHandlerParser extends AbstractCo
element, builder, processor, parserContext);
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, MESSAGE_STORE_ATTRIBUTE);
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "scheduler", "taskScheduler");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, DISCARD_CHANNEL_ATTRIBUTE);
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "lock-registry");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, SEND_TIMEOUT_ATTRIBUTE);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, SEND_PARTIAL_RESULT_ON_EXPIRY_ATTRIBUTE);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "empty-group-min-timeout", "minimumTimeoutForEmptyGroups");
BeanDefinition expressionDef =
IntegrationNamespaceUtils.createExpressionDefinitionFromValueOrExpression("group-timeout", "group-timeout-expression",
parserContext, element, false);
builder.addPropertyValue("groupTimeoutExpression", expressionDef);
}
protected void injectPropertyWithAdapter(String beanRefAttribute, String methodRefAttribute,

View File

@@ -3433,6 +3433,66 @@
<xsd:documentation>A SpEL expression to evaluate against a root object that is the Collection of messages within the message group (e.g, size() > 6)</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="group-timeout" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
A timeout in milliseconds to force the MessageGroup complete,
when the 'ReleaseStrategy' does not 'release' the group when the current Message arrives.
If 'group-timeout' is not provided or is less than '0' the MessageGroup won't be scheduled
to be forced complete.
The action taken when the group is forced complete depends on the
'send-partial-result-on-expiry' attribute.
Mutually exclusive with the 'group-timeout-expression' attribute.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="group-timeout-expression" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
A SpEL expression to evaluate a 'group-timeout' with the MessageGroup as the #root evaluation
context object for scheduling the MessageGroup forced completion,
when the 'ReleaseStrategy' does not 'release' the group when the current Message arrives.
If 'group-timeout-expression' evaluates to 'null' or less than '0',
the MessageGroup won't be scheduled to be forced complete.
The action taken when the group is forced complete depends on the
'send-partial-result-on-expiry' attribute.
Mutually exclusive with the 'group-timeout' attribute.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="scheduler" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.scheduling.TaskScheduler" />
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
Provide a reference to the TaskScheduler instance to schedule 'forceComplete' on
the MessageGroup
when no new message arrives for the MessageGroup within the
'group-timeout' or 'group-timeout-expression'. If it isn't
provided, the default scheduler 'taskScheduler',
registered in the ApplicationContext {ThreadPoolTaskScheduler} will be
used. This attribute only applies if 'group-timeout' or
'group-timeout-expression' is specified.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="lock-registry" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.util.LockRegistry" />
</tool:annotation>
</xsd:appinfo>
<xsd:documentation>
A reference to a 'org.springframework.integration.util.LockRegistry' bean
to obtain 'java.util.concurrent.locks.Lock' by 'groupId'. Used for concurrent operations on
MessageGroups. By default an internal 'DefaultLockRegistry' is used.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="discard-channel" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>

View File

@@ -34,5 +34,15 @@
<aggregator id="nonExpiringAggregator" input-channel="nonExpiringAggregatorInput" output-channel="output"
expire-groups-upon-completion="false" discard-channel="discard"/>
<aggregator id="gta"
input-channel="groupTimeoutAggregatorInput" output-channel="output" discard-channel="discard"
send-partial-result-on-expiry="true"
group-timeout="100"/>
<aggregator input-channel="groupTimeoutExpressionAggregatorInput" output-channel="output" discard-channel="discard"
send-partial-result-on-expiry="true"
group-timeout-expression="size() ge 2 ? 100 : -1"
release-strategy-expression="[0].headers.sequenceNumber == [0].headers.sequenceSize"/>
</beans:beans>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2008 the original author or authors.
* Copyright 2002-2014 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,18 +19,24 @@ package org.springframework.integration.aggregator.integration;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.context.ApplicationContext;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.store.MessageGroupStore;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -39,29 +45,35 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
* @author Iwein Fuld
* @author Alex Peters
* @author Oleg Zhurakousky
* @author Artem Bilan
* @author Gary Russell
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class AggregatorIntegrationTests {
@Autowired
@Qualifier("input")
private ApplicationContext context;
@Autowired
private MessageChannel input;
@Autowired
@Qualifier("expiringAggregatorInput")
private MessageChannel expiringAggregatorInput;
@Autowired
@Qualifier("nonExpiringAggregatorInput")
private MessageChannel nonExpiringAggregatorInput;
@Autowired
@Qualifier("output")
private MessageChannel groupTimeoutAggregatorInput;
@Autowired
private MessageChannel groupTimeoutExpressionAggregatorInput;
@Autowired
private PollableChannel output;
@Autowired
@Qualifier("discard")
private PollableChannel discard;
@Test//(timeout=5000)
@@ -70,7 +82,7 @@ public class AggregatorIntegrationTests {
Map<String, Object> headers = stubHeaders(i, 5, 1);
input.send(new GenericMessage<Integer>(i, headers));
}
assertEquals(0 + 1 + 2 + 3 + 4, output.receive().getPayload());
assertEquals(0 + 1 + 2 + 3 + 4, output.receive(1000).getPayload());
}
@Test
@@ -116,6 +128,66 @@ public class AggregatorIntegrationTests {
}
@Test
public void testGroupTimeoutScheduling() throws Exception {
for (int i = 0; i < 5; i++) {
Map<String, Object> headers = stubHeaders(i, 5, 1);
this.groupTimeoutAggregatorInput.send(new GenericMessage<Integer>(i, headers));
//Wait until 'group-timeout' does its stuff.
MessageGroupStore mgs = TestUtils.getPropertyValue(this.context.getBean("gta.handler"), "messageStore",
MessageGroupStore.class);
int n = 0;
while (n++ < 100 && mgs.getMessageGroupCount() > 0) {
Thread.sleep(100);
}
assertTrue("Group did not complete", n < 100);
assertNotNull(this.output.receive(1000));
assertNull(this.discard.receive(0));
}
}
@Test
public void testGroupTimeoutExpressionScheduling() throws Exception {
// Since group-timeout-expression="size() >= 2 ? 100 : -1". The first message won't be scheduled to 'forceComplete'
this.groupTimeoutExpressionAggregatorInput.send(new GenericMessage<Integer>(1, stubHeaders(1, 6, 1)));
assertNull(this.output.receive(0));
assertNull(this.discard.receive(0));
// As far as 'group.size() >= 2' it will be scheduled to 'forceComplete'
this.groupTimeoutExpressionAggregatorInput.send(new GenericMessage<Integer>(2, stubHeaders(2, 6, 1)));
assertNull(this.output.receive(0));
Message<?> receive = this.output.receive(500);
assertNotNull(receive);
assertEquals(2, ((Collection<?>) receive.getPayload()).size());
assertNull(this.discard.receive(0));
// The same with these three messages
this.groupTimeoutExpressionAggregatorInput.send(new GenericMessage<Integer>(3, stubHeaders(3, 6, 1)));
assertNull(this.output.receive(0));
assertNull(this.discard.receive(0));
this.groupTimeoutExpressionAggregatorInput.send(new GenericMessage<Integer>(4, stubHeaders(4, 6, 1)));
assertNull(this.output.receive(0));
assertNull(this.discard.receive(0));
this.groupTimeoutExpressionAggregatorInput.send(new GenericMessage<Integer>(5, stubHeaders(5, 6, 1)));
assertNull(this.output.receive(0));
receive = this.output.receive(500);
assertNotNull(receive);
assertEquals(3, ((Collection<?>) receive.getPayload()).size());
assertNull(this.discard.receive(0));
// The last message in the sequence - normal release by provided 'ReleaseStrategy'
this.groupTimeoutExpressionAggregatorInput.send(new GenericMessage<Integer>(6, stubHeaders(6, 6, 1)));
receive = this.output.receive(0);
assertNotNull(receive);
assertEquals(1, ((Collection<?>) receive.getPayload()).size());
assertNull(this.discard.receive(0));
}
// configured in context associated with this test
public static class SummingAggregator {
public Integer sum(List<Integer> numbers) {
@@ -127,11 +199,11 @@ public class AggregatorIntegrationTests {
}
}
private Map<String, Object> stubHeaders(int sequenceNumber, int sequenceSize, int correllationId) {
private Map<String, Object> stubHeaders(int sequenceNumber, int sequenceSize, int correlationId) {
Map<String, Object> headers = new HashMap<String, Object>();
headers.put(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, sequenceNumber);
headers.put(IntegrationMessageHeaderAccessor.SEQUENCE_SIZE, sequenceSize);
headers.put(IntegrationMessageHeaderAccessor.CORRELATION_ID, correllationId);
headers.put(IntegrationMessageHeaderAccessor.CORRELATION_ID, correlationId);
return headers;
}

View File

@@ -2,6 +2,6 @@ log4j.rootCategory=WARN, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%c{1}: %m%n
log4j.appender.stdout.layout.ConversionPattern=%d %c{1} [%t] : %m%n
log4j.category.org.springframework.integration=WARN

View File

@@ -368,7 +368,13 @@ then you should simply provide an implementation of the <classname>ReleaseStrate
release-strategy-expression="size() == 5" ]]><co id="aggxml17" /><![CDATA[
expire-groups-upon-completion="false" ]]><co id="aggxml18" /><![CDATA[
empty-group-min-timeout="60000" /> ]]><co id="aggxml19" /><![CDATA[
empty-group-min-timeout="60000" ]]><co id="aggxml19" /><![CDATA[
lock-registry="lockRegistry" ]]><co id="aggxml191" /><![CDATA[
group-timeout="60000" ]]><co id="aggxml20" /><![CDATA[
group-timeout-expression="size() ge 2 ? 100 : -1" ]]><co id="aggxml21" /><![CDATA[
scheduler="taskScheduler" /> ]]><co id="aggxml22" /><![CDATA[
<int:channel id="outputChannel"/>
@@ -435,8 +441,7 @@ then you should simply provide an implementation of the <classname>ReleaseStrate
or by simply invoking that method if you have a reference to the <classname>MessageGroupStore</classname> instance.
Otherwise by itself this attribute has no behavior. It only serves as an indicator of what to do (discard or send to the output/reply
channel) with Messages that are still in the <classname>MessageGroup</classname> that is about to be expired.
<emphasis>Optional</emphasis>.</para>
<para><emphasis>Default - 'false'</emphasis>.</para>
<emphasis>Optional</emphasis>. <emphasis>Default - 'false'</emphasis>.</para>
</callout>
<callout arearefs="aggxml09">
@@ -524,6 +529,61 @@ then you should simply provide an implementation of the <classname>ReleaseStrate
property and it could be as much as this value plus the timeout.</para>
</callout>
<callout arearefs="aggxml191">
<para>
A reference to a <interfacename>org.springframework.integration.util.LockRegistry</interfacename> bean;
used to obtain a <interfacename>Lock</interfacename> based on the <code>groupId</code> for
concurrent operations on the
<code>MessageGroup</code>. By default, an internal <classname>DefaultLockRegistry</classname> is used.
</para>
</callout>
<callout arearefs="aggxml20">
<para>
A timeout in milliseconds to force the <code>MessageGroup</code> complete,
when the <interfacename>ReleaseStrategy</interfacename> doesn't <emphasis>release</emphasis>
the group when the current Message arrives.
This attribute provides a built-in <emphasis>Time-base Release Strategy</emphasis> for the aggregator,
when there is a need to emit a partial result (or discard the group), if a new Message does not arrive
for the <code>MessageGroup</code> within the timeout.
When a new Message arrives at the aggregator, any existing <interfacename>ScheduledFuture&lt;?&gt;</interfacename>
for its <code>MessageGroup</code> is canceled. If the
<interfacename>ReleaseStrategy</interfacename>
returns <code>false</code> (don't release) and the <code>groupTimeout &gt; 0</code> a new task will be
scheduled to expire the group.
Setting this attribute to zero is not advised because it will effectively disable the aggregator because every
message group will be immediately completed. It is possible, however to conditionally set it to zero using an
expression; see <code>group-timeout-expression</code> for information.
The action taken during the completion depends on the <interfacename>ReleaseStrategy</interfacename> and the
<code>send-partial-group-on-expiry</code> attribute. See <xref linkend="agg-and-group-to"/> for
more information.
Mutually exclusive with 'group-timeout-expression' attribute.
</para>
</callout>
<callout arearefs="aggxml21">
<para>
The SpEL expression that evaluates to a <code>groupTimeout</code> with the <code>MessageGroup</code>
as the <code>#root</code> evaluation context object. Used for scheduling the <code>MessageGroup</code> to
be forced complete. If the expression evaluates to null or <code>&lt; 0</code>, the
completion is not scheduled. If it evaluates to zero, the group is completed immediately on
the current thread. In effect, this provides a dynamic <code>group-timeout</code> property.
See <code>group-timeout</code> for more information.
Mutually exclusive with 'group-timeout' attribute.
</para>
</callout>
<callout arearefs="aggxml22">
<para>
A <interfacename>TaskScheduler</interfacename> bean reference to schedule
the <code>MessageGroup</code> to be forced complete
if no new message arrives for the <code>MessageGroup</code>
within the <code>groupTimeout</code>.
If not provided, the default scheduler <code>taskScheduler</code>,
registered in the <interfacename>ApplicationContext</interfacename> (<classname>ThreadPoolTaskScheduler</classname>)
will be used. This attribute does not apply if <code>group-timeout</code> or
<code>group-timeout-expression</code> is not specified.
</para>
</callout>
</calloutlist>
<para>Using a <code>ref</code> attribute is generally recommended if a custom
@@ -663,6 +723,45 @@ then you should simply provide an implementation of the <classname>ReleaseStrate
<interfacename>MessageGroup</interfacename> itself, and you are simply stating
that as soon as there are more than 5 messages in this group, it should be released.
</para>
<section id="agg-and-group-to">
<title>Aggregator and Group Timeout</title>
<para>
Starting with <emphasis>version 4.0</emphasis>, two new mutually exclusive attributes have been introduced:
<code>group-timeout</code> and <code>group-timeout-expression</code> (see the description above). There are some
cases where it is needed to emit the aggregator result (or discard the group) after a timeout
if the <interfacename>ReleaseStrategy</interfacename>
doesn't <emphasis>release</emphasis> when the current Message arrives.
For this purpose the <code>groupTimeout</code> option allows scheduling the <code>MessageGroup</code> to
be forced complete:
<programlisting language="xml"><![CDATA[<aggregator input-channel="input" output-channel="output"
send-partial-result-on-expiry="true"
group-timeout-expression="size() ge 2 ? 10000 : -1"
release-strategy-expression="[0].headers.sequenceNumber == [0].headers.sequenceSize"/>]]></programlisting>
With this example, the normal <emphasis>release</emphasis> will be possible if the aggregator receives the last message
in sequence as defined by the <code>release-strategy-expression</code>. If that specific message does not arrive,
the <code>groupTimeout</code> will force the group complete after 10 seconds as long as the group contains at least 2 Messages.
</para>
<para>
The results of forcing the group complete depends on the <interfacename>ReleaseStrategy</interfacename> and
the <code>send-partial-result-on-expiry</code>.
First, the release strategy is again consulted to see if a <emphasis>normal</emphasis> release is to be made - while the
group won't have changed, the <interfacename>ReleaseStrategy</interfacename> can decide to release the group at this time. If the
release strategy still does not release the group, it will be expired.
If <code>send-partial-result-on-expiry</code> is <code>true</code>,
existing messages in the (partial) <code>MessageGroup</code> will be released as a normal aggregator reply Message to the
<code>output-channel</code>, otherwise it will be discarded.
</para>
<para>
There is a difference between <code>groupTimeout</code> behavior and <classname>MessageGroupStoreReaper</classname>
(see <xref linkend="aggregator-config"/>). The reaper initiates forced completion for all <code>MessageGroup</code>s
in the <code>MessageGroupStore</code> periodically. The <code>groupTimeout</code> does it for each <code>MessageGroup</code>
individually, if a new Message doesn't arrive during the <code>groupTimeout</code>. Also, the reaper can be used to
remove empty groups (empty groups are retained in order to discard late messages,
if <code>expire-groups-upon-completion</code> is false).
</para>
</section>
</section>
<section id="aggregator-annotations">

View File

@@ -52,8 +52,13 @@
release-strategy="releaseStrategyBean" ]]><co id="resxml14-co" linkends="resxml14" /><![CDATA[
release-strategy-method="release" ]]><co id="resxml15-co" linkends="resxml15" /><![CDATA[
release-strategy-expression="size() == 10" ]]><co id="resxml16-co" linkends="resxml16" /><![CDATA[
empty-group-min-timeout="60000" />]]><co id="resxml17-co" linkends="resxml17" /></programlisting>
empty-group-min-timeout="60000" ]]><co id="resxml17-co" linkends="resxml17" /><![CDATA[
lock-registry="lockRegistry" ]]><co id="resxml18" /><![CDATA[
group-timeout="60000" ]]><co id="resxml19" /><![CDATA[
group-timeout-expression="size() ge 2 ? 100 : -1" ]]><co id="resxml20" /><![CDATA[
scheduler="taskScheduler" /> ]]><co id="resxml21" /></programlisting>
<para><calloutlist>
<callout arearefs="resxml1-co" id="resxml1">
<para>The id of the resequencer is
@@ -160,7 +165,27 @@
empty group will also be affected by the reaper's <emphasis>timeout</emphasis>
property and it could be as much as this value plus the timeout.</para>
</callout>
<callout arearefs="resxml18">
<para>
See <xref linkend="aggregator-xml"/>.
</para>
</callout>
<callout arearefs="resxml19">
<para>
See <xref linkend="aggregator-xml"/>.
</para>
</callout>
<callout arearefs="resxml20">
<para>
See <xref linkend="aggregator-xml"/>.
</para>
</callout>
<callout arearefs="resxml21">
<para>
See <xref linkend="aggregator-xml"/>.
</para>
</callout>
</calloutlist></para>
<note>

View File

@@ -209,5 +209,16 @@
For more information see <xref linkend="retry-config"/>.
</para>
</section>
<section id="4.0-release-strategy-group-timeout">
<title>Correlation Endpoint: Time-based Release Strategy</title>
<para>
The mutually exclusive <code>group-timeout</code> and <code>group-timeout-expression</code>
attributes have been added to the <code>&lt;int:aggregator&gt;</code> and <code>&lt;int:resequencer&gt;</code>.
These attributes allow forced completion of a partial <code>MessageGroup</code>,
if the <interfacename>ReleaseStrategy</interfacename> does not release a group and no
further messages arrive within the time specified.
For more information see <xref linkend="aggregator-config"/>.
</para>
</section>
</section>
</chapter>