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:
committed by
Gary Russell
parent
882da62ba0
commit
eb0d1ddc84
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<?></interfacename>
|
||||
for its <code>MessageGroup</code> is canceled. If the
|
||||
<interfacename>ReleaseStrategy</interfacename>
|
||||
returns <code>false</code> (don't release) and the <code>groupTimeout > 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>< 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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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><int:aggregator></code> and <code><int:resequencer></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>
|
||||
|
||||
Reference in New Issue
Block a user