/*
* Copyright 2002-2016 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.aggregator;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.store.MessageGroup;
import org.springframework.integration.store.MessageGroupStore;
import org.springframework.integration.store.SimpleMessageGroup;
import org.springframework.integration.store.SimpleMessageStore;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
/**
* @author Gary Russell
* @author Artem Bilan
* @since 2.2
*
*/
public class AbstractCorrelatingMessageHandlerTests {
@Test // INT-2751
public void testReaperDoesntReapAProcessingGroup() throws Exception {
final MessageGroupStore groupStore = new SimpleMessageStore();
final CountDownLatch waitForSendLatch = new CountDownLatch(1);
final CountDownLatch waitReapStartLatch = new CountDownLatch(1);
final CountDownLatch waitReapCompleteLatch = new CountDownLatch(1);
AbstractCorrelatingMessageHandler handler = new AbstractCorrelatingMessageHandler(group -> group, groupStore) {
@Override
protected void afterRelease(MessageGroup group, Collection<Message<?>> completedMessages) {
}
};
handler.setReleasePartialSequences(true);
/*
* Runs "reap" when group 'bar' is in completion
*/
Executors.newSingleThreadExecutor().execute(() -> {
try {
waitReapStartLatch.await(10, TimeUnit.SECONDS);
}
catch (InterruptedException e1) {
Thread.currentThread().interrupt();
}
waitForSendLatch.countDown();
try {
Thread.sleep(100);
}
catch (InterruptedException e2) {
Thread.currentThread().interrupt();
}
groupStore.expireMessageGroups(50);
waitReapCompleteLatch.countDown();
});
final List<Message<?>> outputMessages = new ArrayList<Message<?>>();
handler.setOutputChannel((message, timeout) -> {
/*
* Executes when group 'bar' completes normally
*/
outputMessages.add(message);
// wake reaper
waitReapStartLatch.countDown();
try {
waitForSendLatch.await(10, TimeUnit.SECONDS);
// wait a little longer for reaper to grab groups
Thread.sleep(2000);
// simulate tx commit
groupStore.removeMessageGroup("bar");
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return true;
});
handler.setReleaseStrategy(group -> group.size() == 2);
QueueChannel discards = new QueueChannel();
handler.setDiscardChannel(discards);
handler.setSendPartialResultOnExpiry(true);
Message<String> message = MessageBuilder.withPayload("foo")
.setCorrelationId("qux")
.build();
// partial group that will be reaped
handler.handleMessage(message);
message = MessageBuilder.withPayload("foo")
.setCorrelationId("bar")
.build();
// full group that should not be reaped
handler.handleMessage(message);
message = MessageBuilder.withPayload("baz")
.setCorrelationId("bar")
.build();
handler.handleMessage(message);
assertTrue(waitReapCompleteLatch.await(10, TimeUnit.SECONDS));
// Before INT-2751 we got bar + bar + qux
assertEquals(2, outputMessages.size()); // bar + qux
// normal release
assertEquals(2, ((MessageGroup) outputMessages.get(0).getPayload()).size()); // 'bar'
// reaper release
assertEquals(1, ((MessageGroup) outputMessages.get(1).getPayload()).size()); // 'qux'
assertNull(discards.receive(0));
}
@Test // INT-2833
public void testReaperReapsAnEmptyGroup() throws Exception {
final MessageGroupStore groupStore = new SimpleMessageStore();
AggregatingMessageHandler handler = new AggregatingMessageHandler(group -> group, groupStore);
final List<Message<?>> outputMessages = new ArrayList<Message<?>>();
handler.setOutputChannel((message, timeout) -> {
/*
* Executes when group 'bar' completes normally
*/
outputMessages.add(message);
return true;
});
handler.setReleaseStrategy(group -> group.size() == 1);
Message<String> message = MessageBuilder.withPayload("foo")
.setCorrelationId("bar")
.build();
handler.handleMessage(message);
assertEquals(1, outputMessages.size());
assertEquals(1, TestUtils.getPropertyValue(handler, "messageStore.groupIdToMessageGroup", Map.class).size());
groupStore.expireMessageGroups(0);
assertEquals(0, TestUtils.getPropertyValue(handler, "messageStore.groupIdToMessageGroup", Map.class).size());
}
@Test // INT-2833
public void testReaperReapsAnEmptyGroupAfterConfiguredDelay() throws Exception {
final MessageGroupStore groupStore = new SimpleMessageStore();
AggregatingMessageHandler handler = new AggregatingMessageHandler(group -> group, groupStore);
final List<Message<?>> outputMessages = new ArrayList<Message<?>>();
handler.setOutputChannel((message, timeout) -> {
/*
* Executes when group 'bar' completes normally
*/
outputMessages.add(message);
return true;
});
handler.setReleaseStrategy(group -> group.size() == 1);
Message<String> message = MessageBuilder.withPayload("foo")
.setCorrelationId("bar")
.build();
handler.handleMessage(message);
handler.setMinimumTimeoutForEmptyGroups(10_000);
assertEquals(1, outputMessages.size());
assertEquals(1, TestUtils.getPropertyValue(handler, "messageStore.groupIdToMessageGroup", Map.class).size());
groupStore.expireMessageGroups(0);
assertEquals(1, TestUtils.getPropertyValue(handler, "messageStore.groupIdToMessageGroup", Map.class).size());
handler.setMinimumTimeoutForEmptyGroups(10);
int n = 0;
while (n++ < 200) {
groupStore.expireMessageGroups(0);
if (TestUtils.getPropertyValue(handler, "messageStore.groupIdToMessageGroup", Map.class).size() > 0) {
Thread.sleep(50);
}
else {
break;
}
}
assertTrue(n < 200);
assertEquals(0, TestUtils.getPropertyValue(handler, "messageStore.groupIdToMessageGroup", Map.class).size());
}
@Test
public void testReapWithChangeInSameMillisecond() throws Exception {
MessageGroupProcessor mgp = new DefaultAggregatingMessageGroupProcessor();
AggregatingMessageHandler handler = new AggregatingMessageHandler(mgp);
handler.setReleaseStrategy(group -> true);
QueueChannel outputChannel = new QueueChannel();
handler.setOutputChannel(outputChannel);
MessageGroupStore mgs = TestUtils.getPropertyValue(handler, "messageStore", MessageGroupStore.class);
Method forceComplete =
AbstractCorrelatingMessageHandler.class.getDeclaredMethod("forceComplete", MessageGroup.class);
forceComplete.setAccessible(true);
GenericMessage<String> secondMessage = new GenericMessage<String>("bar");
mgs.addMessagesToGroup("foo", new GenericMessage<String>("foo"), secondMessage);
MessageGroup group = mgs.getMessageGroup("foo");
// remove a message
mgs.removeMessagesFromGroup("foo", secondMessage);
// force lastModified to be the same
MessageGroup groupNow = mgs.getMessageGroup("foo");
new DirectFieldAccessor(group).setPropertyValue("lastModified", groupNow.getLastModified());
forceComplete.invoke(handler, group);
Message<?> message = outputChannel.receive(0);
assertNotNull(message);
Collection<?> payload = (Collection<?>) message.getPayload();
assertEquals(1, payload.size());
}
@Test /* INT-3216 */
public void testDontReapIfAlreadyComplete() throws Exception {
MessageGroupProcessor mgp = new DefaultAggregatingMessageGroupProcessor();
AggregatingMessageHandler handler = new AggregatingMessageHandler(mgp);
handler.setReleaseStrategy(group -> true);
QueueChannel outputChannel = new QueueChannel();
handler.setOutputChannel(outputChannel);
MessageGroupStore mgs = TestUtils.getPropertyValue(handler, "messageStore", MessageGroupStore.class);
mgs.addMessagesToGroup("foo", new GenericMessage<String>("foo"));
mgs.completeGroup("foo");
mgs = spy(mgs);
new DirectFieldAccessor(handler).setPropertyValue("messageStore", mgs);
Method forceComplete =
AbstractCorrelatingMessageHandler.class.getDeclaredMethod("forceComplete", MessageGroup.class);
forceComplete.setAccessible(true);
MessageGroup group = (MessageGroup) TestUtils.getPropertyValue(mgs, "groupIdToMessageGroup", Map.class)
.get("foo");
assertTrue(group.isComplete());
forceComplete.invoke(handler, group);
verify(mgs, never()).getMessageGroup("foo");
assertNull(outputChannel.receive(0));
}
/*
* INT-3216 - Verifies the complete early exit is taken after a refresh.
*/
@Test
public void testDontReapIfAlreadyCompleteAfterRefetch() throws Exception {
MessageGroupProcessor mgp = new DefaultAggregatingMessageGroupProcessor();
AggregatingMessageHandler handler = new AggregatingMessageHandler(mgp);
handler.setReleaseStrategy(group -> true);
QueueChannel outputChannel = new QueueChannel();
handler.setOutputChannel(outputChannel);
MessageGroupStore mgs = TestUtils.getPropertyValue(handler, "messageStore", MessageGroupStore.class);
mgs.addMessagesToGroup("foo", new GenericMessage<String>("foo"));
MessageGroup group = new SimpleMessageGroup(mgs.getMessageGroup("foo"));
mgs.completeGroup("foo");
mgs = spy(mgs);
new DirectFieldAccessor(handler).setPropertyValue("messageStore", mgs);
Method forceComplete =
AbstractCorrelatingMessageHandler.class.getDeclaredMethod("forceComplete", MessageGroup.class);
forceComplete.setAccessible(true);
MessageGroup groupInStore = (MessageGroup) TestUtils.getPropertyValue(mgs, "groupIdToMessageGroup", Map.class)
.get("foo");
assertTrue(groupInStore.isComplete());
assertFalse(group.isComplete());
new DirectFieldAccessor(group).setPropertyValue("lastModified", groupInStore.getLastModified());
forceComplete.invoke(handler, group);
verify(mgs).getMessageGroup("foo");
assertNull(outputChannel.receive(0));
}
/*
* INT-3216 - Verifies we don't complete if it's a completely new group (different timestamp).
*/
@Test
public void testDontReapIfNewGroupFoundDuringRefetch() throws Exception {
MessageGroupProcessor mgp = new DefaultAggregatingMessageGroupProcessor();
AggregatingMessageHandler handler = new AggregatingMessageHandler(mgp);
handler.setReleaseStrategy(group -> true);
QueueChannel outputChannel = new QueueChannel();
handler.setOutputChannel(outputChannel);
MessageGroupStore mgs = TestUtils.getPropertyValue(handler, "messageStore", MessageGroupStore.class);
mgs.addMessagesToGroup("foo", new GenericMessage<String>("foo"));
MessageGroup group = new SimpleMessageGroup(mgs.getMessageGroup("foo"));
mgs = spy(mgs);
new DirectFieldAccessor(handler).setPropertyValue("messageStore", mgs);
Method forceComplete =
AbstractCorrelatingMessageHandler.class.getDeclaredMethod("forceComplete", MessageGroup.class);
forceComplete.setAccessible(true);
MessageGroup groupInStore = (MessageGroup) TestUtils.getPropertyValue(mgs, "groupIdToMessageGroup", Map.class)
.get("foo");
assertFalse(groupInStore.isComplete());
assertFalse(group.isComplete());
DirectFieldAccessor directFieldAccessor = new DirectFieldAccessor(group);
directFieldAccessor.setPropertyValue("lastModified", groupInStore.getLastModified());
directFieldAccessor.setPropertyValue("timestamp", groupInStore.getTimestamp() - 1);
forceComplete.invoke(handler, group);
verify(mgs).getMessageGroup("foo");
assertNull(outputChannel.receive(0));
}
@Test
public void testInt3483DeadlockOnMessageStoreRemoveMessageGroup() throws InterruptedException {
final AggregatingMessageHandler handler =
new AggregatingMessageHandler(new DefaultAggregatingMessageGroupProcessor());
handler.setOutputChannel(new QueueChannel());
QueueChannel discardChannel = new QueueChannel();
handler.setDiscardChannel(discardChannel);
handler.setReleaseStrategy(group -> true);
handler.setExpireGroupsUponTimeout(false);
SimpleMessageStore messageStore = new SimpleMessageStore() {
@Override
public void removeMessageGroup(Object groupId) {
throw new RuntimeException("intentional");
}
};
handler.setMessageStore(messageStore);
handler.handleMessage(MessageBuilder.withPayload("foo")
.setCorrelationId(1)
.setSequenceNumber(1)
.setSequenceSize(2)
.build());
try {
messageStore.expireMessageGroups(0);
}
catch (Exception e) {
//suppress an intentional 'removeMessageGroup' exception
}
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(() -> handler.handleMessage(MessageBuilder.withPayload("foo")
.setCorrelationId(1)
.setSequenceNumber(2)
.setSequenceSize(2)
.build()));
executorService.shutdown();
/* Previously lock for the groupId hasn't been unlocked from the 'forceComplete', because it wasn't
reachable in case of exception from the BasicMessageGroupStore.removeMessageGroup
*/
assertTrue(executorService.awaitTermination(10, TimeUnit.SECONDS));
/* Since MessageGroup had been marked as 'complete', but hasn't been removed because of exception,
the second message is discarded
*/
Message<?> receive = discardChannel.receive(10000);
assertNotNull(receive);
}
@Test
public void testScheduleRemoveAnEmptyGroupAfterConfiguredDelay() throws Exception {
final MessageGroupStore groupStore = new SimpleMessageStore();
AggregatingMessageHandler handler = new AggregatingMessageHandler(group -> group, groupStore);
final List<Message<?>> outputMessages = new ArrayList<Message<?>>();
handler.setOutputChannel((message, timeout) -> {
/*
* Executes when group 'bar' completes normally
*/
outputMessages.add(message);
return true;
});
handler.setReleaseStrategy(group -> group.size() == 1);
handler.setMinimumTimeoutForEmptyGroups(100);
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.afterPropertiesSet();
handler.setTaskScheduler(taskScheduler);
Message<String> message = MessageBuilder.withPayload("foo")
.setCorrelationId("bar")
.build();
handler.handleMessage(message);
assertEquals(1, outputMessages.size());
assertEquals(1, TestUtils.getPropertyValue(handler, "messageStore.groupIdToMessageGroup", Map.class).size());
Thread.sleep(100);
int n = 0;
while (TestUtils.getPropertyValue(handler, "messageStore.groupIdToMessageGroup", Map.class).size() > 0
&& n++ < 200) {
Thread.sleep(50);
}
assertTrue(n < 200);
assertEquals(0, TestUtils.getPropertyValue(handler, "messageStore.groupIdToMessageGroup", Map.class).size());
}
}