GH-3157: pop sequence for message as well

Fixes https://github.com/spring-projects/spring-integration/issues/3157

* Fixed issue with sequences not being popped when Output processor returns a message.

* Updated documents and added additional Checks to the testcase to ensure proper `popSequence` is done.

* Updated documents and added corrected code formatting

* Updated documents and used headerAccessor Constants for matching headers
* Clean up code style
* Improve docs for the feature on the matter
This commit is contained in:
Jayadev Sirimamilla
2020-01-31 22:48:54 +08:00
committed by Artem Bilan
parent e5740f253c
commit 1d9a818efe
4 changed files with 139 additions and 95 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -38,6 +38,7 @@ import org.springframework.context.Lifecycle;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.StaticMessageHeaderAccessor;
import org.springframework.integration.channel.NullChannel;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.expression.ExpressionUtils;
@@ -59,6 +60,7 @@ import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.core.DestinationResolutionException;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
/**
* Abstract Message handler that holds a buffer of correlated messages in a
@@ -89,6 +91,7 @@ import org.springframework.util.CollectionUtils;
* @author David Liu
* @author Enrique Rodriguez
* @author Meherzad Lahewala
* @author Jayadev Sirimamilla
*
* @since 2.0
*/
@@ -535,43 +538,44 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageP
private void removeEmptyGroupAfterTimeout(MessageGroup messageGroup, long timeout) {
Object groupId = messageGroup.getGroupId();
UUID groupUuid = UUIDConverter.getUUID(groupId);
ScheduledFuture<?> scheduledFuture = getTaskScheduler()
.schedule(() -> {
Lock lock = this.lockRegistry.obtain(groupUuid.toString());
ScheduledFuture<?> scheduledFuture =
getTaskScheduler()
.schedule(() -> {
Lock lock = this.lockRegistry.obtain(groupUuid.toString());
try {
lock.lockInterruptibly();
try {
this.expireGroupScheduledFutures.remove(groupUuid);
/*
* Obtain a fresh state for group from the MessageStore,
* since it could be changed while we have waited for lock.
*/
MessageGroup groupNow = this.messageStore.getMessageGroup(groupUuid);
boolean removeGroup = groupNow.size() == 0 &&
groupNow.getLastModified()
<= (System.currentTimeMillis() - this.minimumTimeoutForEmptyGroups);
if (removeGroup) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Removing empty group: " + groupUuid);
try {
lock.lockInterruptibly();
try {
this.expireGroupScheduledFutures.remove(groupUuid);
/*
* Obtain a fresh state for group from the MessageStore,
* since it could be changed while we have waited for lock.
*/
MessageGroup groupNow = this.messageStore.getMessageGroup(groupUuid);
boolean removeGroup = groupNow.size() == 0 &&
groupNow.getLastModified()
<= (System.currentTimeMillis() - this.minimumTimeoutForEmptyGroups);
if (removeGroup) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Removing empty group: " + groupUuid);
}
remove(messageGroup);
}
}
finally {
lock.unlock();
}
remove(messageGroup);
}
}
finally {
lock.unlock();
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Thread was interrupted while trying to obtain lock."
+ "Rescheduling empty MessageGroup [ " + groupId + "] for removal.");
}
removeEmptyGroupAfterTimeout(messageGroup, timeout);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Thread was interrupted while trying to obtain lock."
+ "Rescheduling empty MessageGroup [ " + groupId + "] for removal.");
}
removeEmptyGroupAfterTimeout(messageGroup, timeout);
}
}, new Date(System.currentTimeMillis() + timeout));
}, new Date(System.currentTimeMillis() + timeout));
if (this.logger.isDebugEnabled()) {
this.logger.debug("Schedule empty MessageGroup [ " + groupId + "] for removal.");
@@ -590,19 +594,20 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageP
final Object groupId = messageGroup.getGroupId();
final long timestamp = messageGroup.getTimestamp();
final long lastModified = messageGroup.getLastModified();
ScheduledFuture<?> scheduledFuture = getTaskScheduler()
.schedule(() -> {
try {
processForceRelease(groupId, timestamp, lastModified);
}
catch (MessageDeliveryException e) {
if (AbstractCorrelatingMessageHandler.this.logger.isWarnEnabled()) {
AbstractCorrelatingMessageHandler.this.logger.warn("The MessageGroup ["
+ groupId + "] is rescheduled by the reason of:", e);
}
scheduleGroupToForceComplete(groupId);
}
}, new Date(System.currentTimeMillis() + groupTimeout));
ScheduledFuture<?> scheduledFuture =
getTaskScheduler()
.schedule(() -> {
try {
processForceRelease(groupId, timestamp, lastModified);
}
catch (MessageDeliveryException e) {
if (AbstractCorrelatingMessageHandler.this.logger.isWarnEnabled()) {
AbstractCorrelatingMessageHandler.this.logger.warn("The MessageGroup ["
+ groupId + "] is rescheduled by the reason of:", e);
}
scheduleGroupToForceComplete(groupId);
}
}, new Date(System.currentTimeMillis() + groupTimeout));
if (this.logger.isDebugEnabled()) {
this.logger.debug("Schedule MessageGroup [ " + messageGroup + "] to 'forceComplete'.");
@@ -768,7 +773,7 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageP
Collection<Message<?>> partialSequence) {
Message<?> lastReleasedMessage = Collections.max(partialSequence, this.sequenceNumberComparator);
return new IntegrationMessageHeaderAccessor(lastReleasedMessage).getSequenceNumber();
return StaticMessageHeaderAccessor.getSequenceNumber(lastReleasedMessage);
}
protected MessageGroup store(Object correlationKey, Message<?> message) {
@@ -830,17 +835,23 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageP
partialSequence = (Collection<Message<?>>) result;
}
if (this.popSequence && partialSequence == null && !(result instanceof Message<?>)) {
AbstractIntegrationMessageBuilder<?> messageBuilder;
if (this.popSequence && partialSequence == null) {
AbstractIntegrationMessageBuilder<?> messageBuilder = null;
if (result instanceof AbstractIntegrationMessageBuilder<?>) {
messageBuilder = (AbstractIntegrationMessageBuilder<?>) result;
}
else {
messageBuilder = getMessageBuilderFactory()
else if (!(result instanceof Message<?>)) {
messageBuilder =
getMessageBuilderFactory()
.withPayload(result)
.copyHeaders(message.getHeaders());
}
result = messageBuilder.popSequenceDetails();
else if (compareSequences((Message<?>) result, message)) {
messageBuilder =
getMessageBuilderFactory()
.fromMessage((Message<?>) result);
}
result = messageBuilder != null ? messageBuilder.popSequenceDetails() : result;
}
}
finally {
@@ -852,6 +863,13 @@ public abstract class AbstractCorrelatingMessageHandler extends AbstractMessageP
return partialSequence;
}
private static boolean compareSequences(Message<?> msg1, Message<?> msg2) {
Object sequence1 = msg1.getHeaders().get(IntegrationMessageHeaderAccessor.SEQUENCE_DETAILS);
Object sequence2 = msg2.getHeaders().get(IntegrationMessageHeaderAccessor.SEQUENCE_DETAILS);
return ObjectUtils.nullSafeEquals(sequence1, sequence2);
}
protected void verifyResultCollectionConsistsOfMessages(Collection<?> elements) {
Class<?> commonElementType = CollectionUtils.findCommonElementType(elements);
Assert.isAssignable(Message.class, commonElementType,

View File

@@ -18,7 +18,6 @@ package org.springframework.integration.dsl.routers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.fail;
import java.util.Arrays;
import java.util.List;
@@ -51,6 +50,7 @@ import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.core.DestinationResolutionException;
@@ -198,7 +198,6 @@ public class RouterTests {
@Test
public void testRecipientListRouter() {
Message<String> fooMessage = MessageBuilder.withPayload("fooPayload").setHeader("recipient", true).build();
Message<String> barMessage = MessageBuilder.withPayload("barPayload").setHeader("recipient", true).build();
Message<String> bazMessage = new GenericMessage<>("baz");
@@ -290,14 +289,9 @@ public class RouterTests {
assertThat(result2b).isNotNull();
assertThat(result2b.getPayload()).isEqualTo("bar");
try {
this.routerMethodInput.send(badMessage);
fail("MessageDeliveryException expected.");
}
catch (MessageDeliveryException e) {
assertThat(e.getMessage()).contains("No channel resolved by router");
}
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> this.routerMethodInput.send(badMessage))
.withMessageContaining("No channel resolved by router");
}
@Test
@@ -319,15 +313,10 @@ public class RouterTests {
assertThat(result2b).isNotNull();
assertThat(result2b.getPayload()).isEqualTo("bar");
try {
this.routerMethod2Input.send(badMessage);
fail("DestinationResolutionException expected.");
}
catch (MessagingException e) {
assertThat(e.getCause()).isInstanceOf(DestinationResolutionException.class);
assertThat(e.getCause().getMessage()).contains("failed to look up MessageChannel with name 'bad-channel'");
}
assertThatExceptionOfType(MessagingException.class)
.isThrownBy(() -> this.routerMethod2Input.send(badMessage))
.withCauseInstanceOf(DestinationResolutionException.class)
.withStackTraceContaining("failed to look up MessageChannel with name 'bad-channel'");
}
@Test
@@ -349,19 +338,14 @@ public class RouterTests {
assertThat(result2b).isNotNull();
assertThat(result2b.getPayload()).isEqualTo("bar");
try {
this.routerMethod3Input.send(badMessage);
fail("DestinationResolutionException expected.");
}
catch (MessagingException e) {
assertThat(e.getCause()).isInstanceOf(DestinationResolutionException.class);
assertThat(e.getCause().getMessage()).contains("failed to look up MessageChannel with name 'bad-channel'");
}
assertThatExceptionOfType(MessagingException.class)
.isThrownBy(() -> this.routerMethod3Input.send(badMessage))
.withCauseInstanceOf(DestinationResolutionException.class)
.withStackTraceContaining("failed to look up MessageChannel with name 'bad-channel'");
}
@Test
public void testMultiRouter() {
Message<String> fooMessage = new GenericMessage<>("foo");
Message<String> barMessage = new GenericMessage<>("bar");
Message<String> badMessage = new GenericMessage<>("bad");
@@ -382,13 +366,9 @@ public class RouterTests {
assertThat(result2b).isNotNull();
assertThat(result2b.getPayload()).isEqualTo("bar");
try {
this.routerMultiInput.send(badMessage);
fail("MessageDeliveryException expected.");
}
catch (MessageDeliveryException e) {
assertThat(e.getMessage()).contains("No channel resolved by router");
}
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> this.routerMultiInput.send(badMessage))
.withMessageContaining("No channel resolved by router");
}
@Autowired
@@ -612,6 +592,34 @@ public class RouterTests {
}
@Autowired
@Qualifier("scatterGatherWireTapChannel")
PollableChannel scatterGatherWireTapChannel;
@Test
public void testNestedScatterGatherSequenceTest() {
PollableChannel replyChannel = new QueueChannel();
this.scatterGatherInSubFlowChannel.send(
MessageBuilder.withPayload("sequencetest")
.setReplyChannel(replyChannel)
.build());
Message<?> wiretapMessage1 = scatterGatherWireTapChannel.receive(10000);
assertThat(wiretapMessage1).isNotNull();
MessageHeaders headers1 = wiretapMessage1.getHeaders();
Message<?> wiretapMessage2 = scatterGatherWireTapChannel.receive(10000);
assertThat(wiretapMessage2).isNotNull()
.extracting(Message::getHeaders)
.isEqualToComparingOnlyGivenFields(headers1, IntegrationMessageHeaderAccessor.CORRELATION_ID,
"gatherResultChannel", IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER,
IntegrationMessageHeaderAccessor.SEQUENCE_SIZE);
Message<?> receive = replyChannel.receive(10000);
assertThat(receive).isNotNull();
assertThat(receive.getPayload()).isEqualTo("sequencetest");
}
@Configuration
@EnableIntegration
@EnableMessageHistory({ "recipientListOrder*", "recipient1*", "recipient2*" })
@@ -903,6 +911,7 @@ public class RouterTests {
.build();
}
@Bean
public IntegrationFlow propagateErrorFromGatherer(TaskExecutor taskExecutor) {
return IntegrationFlows.from(Function.class)
@@ -920,14 +929,21 @@ public class RouterTests {
.get();
}
@Bean
public PollableChannel scatterGatherWireTapChannel() {
return new QueueChannel();
}
@Bean
public IntegrationFlow scatterGatherInSubFlow() {
return flow -> flow.scatterGather(s -> s.applySequence(true)
.recipientFlow(inflow -> inflow
.recipientFlow(inflow -> inflow.wireTap(scatterGatherWireTapChannel())
.scatterGather(s1 -> s1.applySequence(true)
.recipientFlow(IntegrationFlowDefinition::bridge),
.recipientFlow(IntegrationFlowDefinition::bridge)
.recipientFlow("sequencetest"::equals,
IntegrationFlowDefinition::bridge),
g -> g.outputProcessor(MessageGroup::getOne)
)),
).wireTap(scatterGatherWireTapChannel()).bridge()),
g -> g.outputProcessor(MessageGroup::getOne));
}