From d2580a3b0b51bc9f4f681cdae13215f240b292e2 Mon Sep 17 00:00:00 2001 From: Mark Fisher Date: Wed, 27 Feb 2008 02:33:44 +0000 Subject: [PATCH] Major refactoring of AggregatingMessageHandler and related classes. --- .../router/AggregatingMessageHandler.java | 252 +++++++++++++++--- .../router/AggregationBarrier.java | 96 +++++++ ...nStrategy.java => CompletionStrategy.java} | 4 +- .../integration/router/MessageBarrier.java | 34 +++ .../integration/router/RoutingBarrier.java | 104 -------- .../SequenceSizeCompletionStrategy.java | 4 +- .../AggregatingMessageHandlerTests.java | 79 +++++- .../router/AggregationBarrierTests.java | 76 ++++++ .../router/RoutingBarrierTests.java | 87 ------ 9 files changed, 491 insertions(+), 245 deletions(-) create mode 100644 spring-integration-core/src/main/java/org/springframework/integration/router/AggregationBarrier.java rename spring-integration-core/src/main/java/org/springframework/integration/router/{RoutingBarrierCompletionStrategy.java => CompletionStrategy.java} (92%) create mode 100644 spring-integration-core/src/main/java/org/springframework/integration/router/MessageBarrier.java delete mode 100644 spring-integration-core/src/main/java/org/springframework/integration/router/RoutingBarrier.java create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/router/AggregationBarrierTests.java delete mode 100644 spring-integration-core/src/test/java/org/springframework/integration/router/RoutingBarrierTests.java diff --git a/spring-integration-core/src/main/java/org/springframework/integration/router/AggregatingMessageHandler.java b/spring-integration-core/src/main/java/org/springframework/integration/router/AggregatingMessageHandler.java index 92a2255ad5..b042a72835 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/router/AggregatingMessageHandler.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/router/AggregatingMessageHandler.java @@ -16,12 +16,26 @@ package org.springframework.integration.router; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.integration.channel.MessageChannel; import org.springframework.integration.handler.MessageHandler; import org.springframework.integration.message.Message; import org.springframework.integration.message.MessageHandlingException; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** * A {@link MessageHandler} implementation that waits for a complete @@ -35,93 +49,249 @@ import org.springframework.util.Assert; *

* The default strategy for determining whether a group is complete is based on * the 'sequenceSize' property of the header. Alternatively, a - * custom implementation of the {@link RoutingBarrierCompletionStrategy} may be - * provided. + * custom implementation of the {@link CompletionStrategy} may be provided. *

* The 'timeout' value determines how long to wait for the * complete group after the arrival of the first {@link Message} of the group. * The default value is 1 minute. If the timeout elapses prior to completion, - * the handler will throw a {@link MessageHandlingException} by default. To - * prevent the exception and aggregate the group even when incomplete, set the - * 'shouldFailOnTimeout' property to 'false'. + * then Messages with that timed-out 'correlationId' will be sent to the + * 'discardChannel' if provided. * * @author Mark Fisher + * @author Marius Bogoevici */ -public class AggregatingMessageHandler implements MessageHandler { +public class AggregatingMessageHandler implements MessageHandler, InitializingBean { - private long timeout = 60000; + private final Log logger = LogFactory.getLog(this.getClass()); - private boolean shouldFailOnTimeout = true; + private final Aggregator aggregator; - private Aggregator aggregator; + private volatile MessageChannel defaultReplyChannel; - private RoutingBarrierCompletionStrategy completionStrategy = new SequenceSizeCompletionStrategy(); + private volatile MessageChannel discardChannel; - private ConcurrentHashMap barriers = new ConcurrentHashMap(); + private volatile long sendTimeout = 1000; + + private volatile CompletionStrategy completionStrategy = new SequenceSizeCompletionStrategy(); + + private final ConcurrentMap barriers = new ConcurrentHashMap(); + + private volatile long timeout = 60000; + + private volatile boolean sendPartialResultOnTimeout = false; + + private volatile long reaperInterval = 1000; + + private volatile int trackedCorrelationIdCapacity = 1000; + + private volatile BlockingQueue trackedCorrelationIds; + + private final ScheduledExecutorService executor; + + private volatile boolean initialized; /** * Create a handler that delegates to the provided aggregator to combine a - * group of messages into a single message. + * group of messages into a single message. The executor will be used for + * scheduling a background maintenance thread. If null, a new + * single-threaded executor will be created. */ - public AggregatingMessageHandler(Aggregator aggregator) { + public AggregatingMessageHandler(Aggregator aggregator, ScheduledExecutorService executor) { Assert.notNull(aggregator, "'aggregator' must not be null"); this.aggregator = aggregator; + this.executor = (executor != null) ? executor : Executors.newSingleThreadScheduledExecutor(); + } + + public AggregatingMessageHandler(Aggregator aggregator) { + this(aggregator, null); } + /** + * Set the default channel for sending aggregated Messages. Note that + * precedence will be given to the 'returnAddress' of the aggregated + * message itself, then to the 'returnAddress' of the original message. + */ + public void setDefaultReplyChannel(MessageChannel defaultReplyChannel) { + this.defaultReplyChannel = defaultReplyChannel; + } + + /** + * Specify a channel for sending Messages that arrive after their aggregation + * group has either completed or timed-out. + */ + public void setDiscardChannel(MessageChannel discardChannel) { + this.discardChannel = discardChannel; + } + + /** + * Set the timeout for sending aggregation results and discarded Messages. + */ + public void setSendTimeout(long sendTimeout) { + this.sendTimeout = sendTimeout; + } + + /** + * Specify whether to aggregate and send the resulting Message when the + * timeout elapses prior to the CompletionStrategy. + */ + public void setSendPartialResultOnTimeout(boolean sendPartialResultOnTimeout) { + this.sendPartialResultOnTimeout = sendPartialResultOnTimeout; + } + + /** + * Set the interval in milliseconds for the reaper thread. Default is 1000. + */ + public void setReaperInterval(long reaperInterval) { + Assert.isTrue(reaperInterval > 0, "'reaperInterval' must be a positive value"); + this.reaperInterval = reaperInterval; + } + + /** + * Set the number of completed correlationIds to track. Default is 1000. + */ + public void setTrackedCorrelationIdCapacity(int trackedCorrelationIdCapacity) { + Assert.isTrue(trackedCorrelationIdCapacity > 0, "'trackedCorrelationIdCapacity' must be a positive value"); + this.trackedCorrelationIdCapacity = trackedCorrelationIdCapacity; + } + + /** + * Initialize this handler. + */ + public void afterPropertiesSet() { + this.trackedCorrelationIds = new ArrayBlockingQueue(this.trackedCorrelationIdCapacity); + this.executor.scheduleWithFixedDelay(new ReaperTask(), + this.reaperInterval, this.reaperInterval, TimeUnit.MILLISECONDS); + this.initialized = true; + } + /** * Strategy to determine whether the group of messages is complete. */ - public void setCompletionStrategy(RoutingBarrierCompletionStrategy completionStrategy) { + public void setCompletionStrategy(CompletionStrategy completionStrategy) { Assert.notNull(completionStrategy, "'completionStrategy' must not be null"); this.completionStrategy = completionStrategy; } /** * Maximum time to wait (in milliseconds) for the completion strategy to - * become true. + * become true. The default is 60000 (1 minute). */ public void setTimeout(long timeout) { Assert.isTrue(timeout >= 0, "'timeout' must not be negative"); this.timeout = timeout; } - /** - * Specify whether this handler should throw a {@link MessageHandlingException} - * when a message group does not reach completion within the allotted time. The - * default is 'true'. Setting this to 'false' will cause - * the {@link Aggregator} to be invoked even when the group is incomplete. - */ - public void setShouldFailOnTimeout(boolean setShouldFailOnTimeout) { - this.shouldFailOnTimeout = setShouldFailOnTimeout; - } - public Message handle(Message message) { + if (!this.initialized) { + this.afterPropertiesSet(); + } Object correlationId = message.getHeader().getCorrelationId(); if (correlationId == null) { throw new MessageHandlingException(this.getClass().getSimpleName() + " requires the 'correlationId' property"); } - RoutingBarrier barrier = barriers.putIfAbsent(correlationId, new RoutingBarrier(this.completionStrategy)); - if (barrier == null) { - try { - barrier = barriers.get(correlationId); - barrier.addMessage(message); - if (!barrier.waitForCompletion(this.timeout) && this.shouldFailOnTimeout) { - throw new MessageHandlingException("aggregation did not complete " - + "within the allotted time limit of " + this.timeout + " milliseconds"); - } - Message result = aggregator.aggregate(barrier.getMessages()); - return result; + if (this.trackedCorrelationIds.contains(correlationId)) { + if (logger.isDebugEnabled()) { + logger.debug("Aggregation for correlationId '" + correlationId + + "' has already completed or timed out."); } - finally { - this.barriers.remove(correlationId); + this.sendToDiscardChannelIfAvailable(message); + return null; + } + AggregationBarrier barrier = barriers.putIfAbsent(correlationId, + new AggregationBarrier(this.completionStrategy)); + if (barrier == null) { + barrier = barriers.get(correlationId); + } + List> releasedMessages = barrier.addAndRelease(message); + if (CollectionUtils.isEmpty(releasedMessages)) { + return null; + } + this.removeBarrier(correlationId); + this.aggregationCompleted(correlationId, releasedMessages); + return null; + } + + private void sendToDiscardChannelIfAvailable(Message message) { + if (this.discardChannel != null) { + if (!this.discardChannel.send(message, this.sendTimeout)) { + if (logger.isWarnEnabled()) { + logger.warn("unable to send to 'discardChannel', message: " + message); + } } } - else { - barriers.get(correlationId).addMessage(message); - return null; + } + + private void aggregationCompleted(Object correlationId, List> messages) { + if (CollectionUtils.isEmpty(messages)) { + if (logger.isDebugEnabled()) { + logger.debug("no messages to aggregate"); + } + return; + } + Message result = aggregator.aggregate(messages); + MessageChannel replyChannel = this.resolveReplyChannelFromMessage(result); + if (replyChannel == null) { + replyChannel = this.resolveReplyChannelFromMessage(messages.get(0)); + if (replyChannel == null) { + replyChannel = this.defaultReplyChannel; + } + } + if (replyChannel != null) { + replyChannel.send(result, this.sendTimeout); + } + else if (logger.isWarnEnabled()) { + logger.warn("unable to determine reply channel for aggregation result: " + result); + } + } + + private void removeBarrier(Object correlationId) { + if (this.barriers.remove(correlationId) != null) { + synchronized (this.trackedCorrelationIds) { + boolean added = this.trackedCorrelationIds.offer(correlationId); + if (!added) { + this.trackedCorrelationIds.poll(); + this.trackedCorrelationIds.offer(correlationId); + } + } + } + } + + private MessageChannel resolveReplyChannelFromMessage(Message message) { + Object returnAddress = message.getHeader().getReturnAddress(); + if (returnAddress != null) { + if (returnAddress instanceof MessageChannel) { + return (MessageChannel) returnAddress; + } + if (logger.isWarnEnabled()) { + logger.warn("Aggregator can only reply to a 'returnAddress' of type MessageChannel."); + } + } + return null; + } + + + private class ReaperTask implements Runnable { + + public void run() { + long currentTime = System.currentTimeMillis(); + for (Map.Entry entry : barriers.entrySet()) { + if (currentTime - entry.getValue().getTimestamp() >= timeout) { + Object correlationId = entry.getKey(); + List> messages = entry.getValue().getMessages(); + removeBarrier(correlationId); + if (sendPartialResultOnTimeout) { + aggregationCompleted(correlationId, messages); + } + else { + for (Message message : messages) { + sendToDiscardChannelIfAvailable(message); + } + } + } + } } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/router/AggregationBarrier.java b/spring-integration-core/src/main/java/org/springframework/integration/router/AggregationBarrier.java new file mode 100644 index 0000000000..0b7d2553a8 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/router/AggregationBarrier.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2007 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.router; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.integration.message.Message; +import org.springframework.util.Assert; + +/** + * MessageBarrier implementation for message aggregation. Delegates to a + * {@link CompletionStrategy} to determine when the group of messages is ready + * for aggregation. + * + * @author Marius Bogoevici + * @author Mark Fisher + */ +public class AggregationBarrier implements MessageBarrier { + + private final Log logger = LogFactory.getLog(this.getClass()); + + private final List> messages = new CopyOnWriteArrayList>(); + + private final CompletionStrategy completionStrategy; + + private volatile boolean complete = false; + + private final ReentrantLock lock = new ReentrantLock(); + + private final long timestamp = System.currentTimeMillis(); + + + /** + * Create an AggregationBarrier with the given {@link CompletionStrategy}. + */ + public AggregationBarrier(CompletionStrategy completionStrategy) { + Assert.notNull(completionStrategy, "'completionStrategy' must not be null"); + this.completionStrategy = completionStrategy; + } + + + /** + * Returns the creation time of this barrier as the number of milliseconds + * since January 1, 1970. + * @see java.lang.System#currentTimeMillis() + */ + public long getTimestamp() { + return this.timestamp; + } + + /** + * Adds a message to the aggregation group and releases if complete. + * Otherwise, the return value will be null. + */ + public List> addAndRelease(Message message) { + try { + this.lock.lock(); + if (this.complete) { + if (logger.isDebugEnabled()) { + logger.debug("Message received after aggregation has already completed: " + message); + } + return null; + } + this.messages.add(message); + boolean complete = completionStrategy.isComplete(this.messages); + return (complete) ? this.messages : null; + } + finally { + this.lock.unlock(); + } + } + + public List> getMessages() { + return this.messages; + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/router/RoutingBarrierCompletionStrategy.java b/spring-integration-core/src/main/java/org/springframework/integration/router/CompletionStrategy.java similarity index 92% rename from spring-integration-core/src/main/java/org/springframework/integration/router/RoutingBarrierCompletionStrategy.java rename to spring-integration-core/src/main/java/org/springframework/integration/router/CompletionStrategy.java index 0c9d970f77..bd7eac2fd4 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/router/RoutingBarrierCompletionStrategy.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/router/CompletionStrategy.java @@ -25,9 +25,9 @@ import org.springframework.integration.message.Message; * completion (i.e. can trip a barrier). * * @author Mark Fisher - * @see RoutingBarrier + * @see AggregationBarrier */ -public interface RoutingBarrierCompletionStrategy { +public interface CompletionStrategy { boolean isComplete(List> messages); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/router/MessageBarrier.java b/spring-integration-core/src/main/java/org/springframework/integration/router/MessageBarrier.java new file mode 100644 index 0000000000..9d3bf22e61 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/router/MessageBarrier.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2007 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.router; + +import java.util.List; + +import org.springframework.integration.message.Message; + +/** + * Common interface for routing components that release a list of + * {@link Message Messages} based upon a condition that is met when a + * {@link Message} arrives. + * + * @author Mark Fisher + */ +public interface MessageBarrier { + + List> addAndRelease(Message message); + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/router/RoutingBarrier.java b/spring-integration-core/src/main/java/org/springframework/integration/router/RoutingBarrier.java deleted file mode 100644 index 650f9e107b..0000000000 --- a/spring-integration-core/src/main/java/org/springframework/integration/router/RoutingBarrier.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2002-2007 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.router; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.ReentrantLock; - -import org.springframework.integration.message.Message; -import org.springframework.util.Assert; - -/** - * A rendezvous point for {@link Message Messages} that delegates to a - * {@link RoutingBarrierCompletionStrategy} to determine when a - * complete message group is available. - * - * @author Mark Fisher - */ -public class RoutingBarrier { - - private final List> messages = new CopyOnWriteArrayList>(); - - private final RoutingBarrierCompletionStrategy completionStrategy; - - private volatile boolean complete = false; - - private final ReentrantLock lock = new ReentrantLock(); - - private final Condition condition = lock.newCondition(); - - - public RoutingBarrier(RoutingBarrierCompletionStrategy completionStrategy) { - Assert.notNull(completionStrategy, "'completionStrategy' must not be null"); - this.completionStrategy = completionStrategy; - } - - - public void addMessage(Message message) { - this.messages.add(message); - if (this.completionStrategy.isComplete(this.messages)) { - try { - this.lock.lock(); - if (!this.complete) { - this.complete = true; - this.condition.signalAll(); - } - } - finally { - this.lock.unlock(); - } - } - } - - public boolean waitForCompletion(long timeout) { - if (this.complete) { - return true; - } - lock.lock(); - try { - if (this.complete) { - return true; - } - if (timeout >= 0) { - return this.condition.await(timeout, TimeUnit.MILLISECONDS); - } - else { - this.condition.await(); - return true; - } - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return false; - } - finally { - lock.unlock(); - } - } - - public boolean isComplete() { - return this.complete; - } - - public List> getMessages() { - return this.messages; - } - -} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/router/SequenceSizeCompletionStrategy.java b/spring-integration-core/src/main/java/org/springframework/integration/router/SequenceSizeCompletionStrategy.java index 229c430e4a..85fe7a45d7 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/router/SequenceSizeCompletionStrategy.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/router/SequenceSizeCompletionStrategy.java @@ -22,13 +22,13 @@ import org.springframework.integration.message.Message; import org.springframework.util.CollectionUtils; /** - * An implementation of {@link RoutingBarrierCompletionStrategy} that simply + * An implementation of {@link CompletionStrategy} that simply * compares the current size of the message list to the expected 'sequenceSize' * according to the first {@link Message} in the list. * * @author Mark Fisher */ -public class SequenceSizeCompletionStrategy implements RoutingBarrierCompletionStrategy { +public class SequenceSizeCompletionStrategy implements CompletionStrategy { public boolean isComplete(List> messages) { if (CollectionUtils.isEmpty(messages)) { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/router/AggregatingMessageHandlerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/router/AggregatingMessageHandlerTests.java index 35cb578f64..4bf7ac2c28 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/router/AggregatingMessageHandlerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/router/AggregatingMessageHandlerTests.java @@ -68,26 +68,31 @@ public class AggregatingMessageHandlerTests { } @Test - public void testShouldFailOnTimeoutByDefault() throws InterruptedException { + public void testShouldNotSendPartialResultOnTimeoutByDefault() throws InterruptedException { + SimpleChannel discardChannel = new SimpleChannel(); AggregatingMessageHandler aggregator = new AggregatingMessageHandler(new TestAggregator()); - aggregator.setTimeout(10); + aggregator.setTimeout(50); + aggregator.setReaperInterval(10); + aggregator.setDiscardChannel(discardChannel); SimpleChannel replyChannel = new SimpleChannel(); - Message message1 = createMessage("123", "ABC", 2, 1, replyChannel); + Message message = createMessage("123", "ABC", 2, 1, replyChannel); CountDownLatch latch = new CountDownLatch(1); - AggregatorTestTask task = new AggregatorTestTask(aggregator, message1, latch); + AggregatorTestTask task = new AggregatorTestTask(aggregator, message, latch); executor.execute(task); latch.await(1000, TimeUnit.MILLISECONDS); Message reply = replyChannel.receive(0); assertNull(reply); - assertNotNull(task.getException()); - assertEquals(MessageHandlingException.class, task.getException().getClass()); + Message discardedMessage = discardChannel.receive(500); + assertNotNull(discardedMessage); + assertEquals(message, discardedMessage); } @Test - public void testShouldFailOnTimeoutFalse() throws InterruptedException { + public void testShouldSendPartialResultOnTimeoutTrue() throws InterruptedException { AggregatingMessageHandler aggregator = new AggregatingMessageHandler(new TestAggregator()); - aggregator.setTimeout(10); - aggregator.setShouldFailOnTimeout(false); + aggregator.setTimeout(50); + aggregator.setReaperInterval(10); + aggregator.setSendPartialResultOnTimeout(true); SimpleChannel replyChannel = new SimpleChannel(); Message message1 = createMessage("123", "ABC", 3, 1, replyChannel); Message message2 = createMessage("456", "ABC", 3, 2, replyChannel); @@ -131,6 +136,62 @@ public class AggregatingMessageHandlerTests { assertEquals("abcdefghi", reply2.getPayload()); } + @Test + public void testDiscardChannelForTrackedCorrelationId() { + SimpleChannel replyChannel = new SimpleChannel(); + SimpleChannel discardChannel = new SimpleChannel(); + AggregatingMessageHandler aggregator = new AggregatingMessageHandler(new TestAggregator()); + aggregator.setDiscardChannel(discardChannel); + aggregator.handle(createMessage("test-1a", 1, 1, 1, replyChannel)); + assertEquals("test-1a", replyChannel.receive(100).getPayload()); + aggregator.handle(createMessage("test-1b", 1, 1, 1, replyChannel)); + assertEquals("test-1b", discardChannel.receive(100).getPayload()); + } + + @Test + public void testTrackedCorrelationIdsCapacityAtLimit() { + SimpleChannel replyChannel = new SimpleChannel(); + SimpleChannel discardChannel = new SimpleChannel(); + AggregatingMessageHandler aggregator = new AggregatingMessageHandler(new TestAggregator()); + aggregator.setTrackedCorrelationIdCapacity(3); + aggregator.setDiscardChannel(discardChannel); + aggregator.handle(createMessage("test-1a", 1, 1, 1, replyChannel)); + assertEquals("test-1a", replyChannel.receive(100).getPayload()); + aggregator.handle(createMessage("test-2", 2, 1, 1, replyChannel)); + assertEquals("test-2", replyChannel.receive(100).getPayload()); + aggregator.handle(createMessage("test-3", 3, 1, 1, replyChannel)); + assertEquals("test-3", replyChannel.receive(100).getPayload()); + aggregator.handle(createMessage("test-1b", 1, 1, 1, replyChannel)); + assertEquals("test-1b", discardChannel.receive(100).getPayload()); + } + + @Test + public void testTrackedCorrelationIdsCapacityPassesLimit() { + SimpleChannel replyChannel = new SimpleChannel(); + SimpleChannel discardChannel = new SimpleChannel(); + AggregatingMessageHandler aggregator = new AggregatingMessageHandler(new TestAggregator()); + aggregator.setTrackedCorrelationIdCapacity(3); + aggregator.setDiscardChannel(discardChannel); + aggregator.handle(createMessage("test-1a", 1, 1, 1, replyChannel)); + assertEquals("test-1a", replyChannel.receive(100).getPayload()); + aggregator.handle(createMessage("test-2", 2, 1, 1, replyChannel)); + assertEquals("test-2", replyChannel.receive(100).getPayload()); + aggregator.handle(createMessage("test-3", 3, 1, 1, replyChannel)); + assertEquals("test-3", replyChannel.receive(100).getPayload()); + aggregator.handle(createMessage("test-4", 4, 1, 1, replyChannel)); + assertEquals("test-4", replyChannel.receive(100).getPayload()); + aggregator.handle(createMessage("test-1b", 1, 1, 1, replyChannel)); + assertEquals("test-1b", replyChannel.receive(100).getPayload()); + assertNull(discardChannel.receive(0)); + } + + @Test(expected=MessageHandlingException.class) + public void testExceptionThrownIfNoCorrelationId() throws InterruptedException { + AggregatingMessageHandler aggregator = new AggregatingMessageHandler(new TestAggregator()); + Message message = createMessage("123", null, 2, 1, new SimpleChannel()); + aggregator.handle(message); + } + private static Message createMessage(String payload, Object correlationId, int sequenceSize, int sequenceNumber, MessageChannel replyChannel) { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/router/AggregationBarrierTests.java b/spring-integration-core/src/test/java/org/springframework/integration/router/AggregationBarrierTests.java new file mode 100644 index 0000000000..f15ffd3e10 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/router/AggregationBarrierTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2007 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.router; + +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.List; + +import org.junit.Test; + +import org.springframework.integration.message.Message; +import org.springframework.integration.message.StringMessage; + +/** + * @author Mark Fisher + */ +public class AggregationBarrierTests { + + @Test + public void testBasicCompletionCheck() { + AggregationBarrier barrier = new AggregationBarrier(new TwoMessageCompletionStrategy()); + assertNull(barrier.addAndRelease(new StringMessage("test1"))); + assertNotNull(barrier.addAndRelease(new StringMessage("test2"))); + } + + @Test + public void testMessageRetrieval() { + AggregationBarrier barrier = new AggregationBarrier(new TwoMessageCompletionStrategy()); + barrier.addAndRelease(new StringMessage("test1")); + assertEquals(1, barrier.getMessages().size()); + barrier.addAndRelease(new StringMessage("test2")); + assertEquals(2, barrier.getMessages().size()); + } + + @Test + public void testTimestamp() { + long before = System.currentTimeMillis(); + AggregationBarrier barrier = new AggregationBarrier(new TwoMessageCompletionStrategy()); + long timestamp = barrier.getTimestamp(); + assertTrue(before <= timestamp); + long after = System.currentTimeMillis(); + assertTrue(after >= timestamp); + } + + @Test + public void testEmptyMessageList() { + AggregationBarrier barrier = new AggregationBarrier(new TwoMessageCompletionStrategy()); + assertEquals(0, barrier.getMessages().size()); + } + + + private static class TwoMessageCompletionStrategy implements CompletionStrategy { + + public boolean isComplete(List> messages) { + return (messages.size() == 2); + } + } + +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/router/RoutingBarrierTests.java b/spring-integration-core/src/test/java/org/springframework/integration/router/RoutingBarrierTests.java deleted file mode 100644 index 0cb367f863..0000000000 --- a/spring-integration-core/src/test/java/org/springframework/integration/router/RoutingBarrierTests.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2002-2007 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.router; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.util.List; - -import org.junit.Test; - -import org.springframework.integration.message.Message; -import org.springframework.integration.message.StringMessage; - -/** - * @author Mark Fisher - */ -public class RoutingBarrierTests { - - @Test - public void testBasicCompletionCheck() { - RoutingBarrier barrier = new RoutingBarrier(new TwoMessageCompletionStrategy()); - barrier.addMessage(new StringMessage("test1")); - assertFalse(barrier.isComplete()); - barrier.addMessage(new StringMessage("test2")); - assertTrue(barrier.isComplete()); - } - - @Test - public void testMessageRetrieval() { - RoutingBarrier barrier = new RoutingBarrier(new TwoMessageCompletionStrategy()); - barrier.addMessage(new StringMessage("test1")); - assertEquals(1, barrier.getMessages().size()); - barrier.addMessage(new StringMessage("test2")); - assertEquals(2, barrier.getMessages().size()); - } - - @Test - public void testWaitForCompletionTimesOut() { - RoutingBarrier barrier = new RoutingBarrier(new TwoMessageCompletionStrategy()); - barrier.addMessage(new StringMessage("test1")); - assertFalse(barrier.isComplete()); - assertFalse(barrier.waitForCompletion(10)); - } - - @Test - public void testWaitForCompletionReturnsTrueImmediately() { - RoutingBarrier barrier = new RoutingBarrier(new TwoMessageCompletionStrategy()); - barrier.addMessage(new StringMessage("test1")); - assertFalse(barrier.isComplete()); - barrier.addMessage(new StringMessage("test2")); - assertTrue(barrier.waitForCompletion(0)); - assertTrue(barrier.isComplete()); - } - - @Test - public void testEmptyMessageList() { - RoutingBarrier barrier = new RoutingBarrier(new TwoMessageCompletionStrategy()); - assertFalse(barrier.isComplete()); - assertFalse(barrier.waitForCompletion(0)); - assertEquals(0, barrier.getMessages().size()); - } - - - private static class TwoMessageCompletionStrategy implements RoutingBarrierCompletionStrategy { - - public boolean isComplete(List> messages) { - return (messages.size() == 2); - } - } - -}