/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.camel.component.sjms.batch;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import javax.jms.ConnectionFactory;
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.LoggingLevel;
import org.apache.camel.Processor;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.component.sjms.SjmsComponent;
import org.apache.camel.component.sjms.support.MockConnectionFactory;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.impl.SimpleRegistry;
import org.apache.camel.test.junit4.CamelTestSupport;
import org.apache.camel.util.StopWatch;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SjmsBatchConsumerTest extends CamelTestSupport {
private static final Logger LOG = LoggerFactory.getLogger(SjmsBatchConsumerTest.class);
@Rule
public EmbeddedActiveMQBroker broker = new EmbeddedActiveMQBroker("localhost");
@Override
public CamelContext createCamelContext() throws Exception {
SimpleRegistry registry = new SimpleRegistry();
registry.put("testStrategy", new ListAggregationStrategy());
// the only thing special about this MockConnectionFactor is it allows us to call returnBadSessionNTimes(int)
// which will cause the MockSession to throw an IllegalStateException <int> times before returning a valid one.
// This gives us the ability to test bad sessions
ConnectionFactory connectionFactory = new MockConnectionFactory(broker.getTcpConnectorUri());
SjmsComponent sjmsComponent = new SjmsComponent();
sjmsComponent.setConnectionFactory(connectionFactory);
SjmsBatchComponent sjmsBatchComponent = new SjmsBatchComponent();
sjmsBatchComponent.setConnectionFactory(connectionFactory);
CamelContext context = new DefaultCamelContext(registry);
context.addComponent("sjms", sjmsComponent);
context.addComponent("sjms-batch", sjmsBatchComponent);
return context;
}
private static class TransactedSendHarness extends RouteBuilder {
private final String queueName;
TransactedSendHarness(String queueName) {
this.queueName = queueName;
}
@Override
public void configure() throws Exception {
from("direct:in").routeId("harness").startupOrder(20)
.split(body())
.toF("sjms:queue:%s?transacted=true", queueName)
.to("mock:before")
.end();
}
}
@Override
public boolean isUseAdviceWith() {
return true;
}
@Test
public void testConsumption() throws Exception {
final int messageCount = 10000;
final int consumerCount = 5;
final String queueName = getQueueName();
context.addRoutes(new TransactedSendHarness(queueName));
context.addRoutes(new RouteBuilder() {
public void configure() throws Exception {
int completionTimeout = 1000;
int completionSize = 200;
fromF("sjms-batch:%s?completionTimeout=%s&completionSize=%s&consumerCount=%s&aggregationStrategy=#testStrategy",
queueName, completionTimeout, completionSize, consumerCount)
.routeId("batchConsumer").startupOrder(10).autoStartup(false)
.split(body())
.to("mock:split");
}
});
context.start();
MockEndpoint mockBefore = getMockEndpoint("mock:before");
mockBefore.setExpectedMessageCount(messageCount);
MockEndpoint mockSplit = getMockEndpoint("mock:split");
mockSplit.setExpectedMessageCount(messageCount);
LOG.info("Sending messages");
template.sendBody("direct:in", generateStrings(messageCount));
LOG.info("Send complete");
StopWatch stopWatch = new StopWatch();
context.startRoute("batchConsumer");
assertMockEndpointsSatisfied();
long time = stopWatch.stop();
LOG.info("Processed {} messages in {} ms", messageCount, time);
LOG.info("Average throughput {} msg/s", (long) (messageCount / (time / 1000d)));
}
@Test
public void testConsumptionCompletionSize() throws Exception {
final int completionSize = 5;
final int completionTimeout = -1; // size-based only
final String queueName = getQueueName();
context.addRoutes(new TransactedSendHarness(queueName));
context.addRoutes(new RouteBuilder() {
public void configure() throws Exception {
fromF("sjms-batch:%s?completionTimeout=%s&completionSize=%s&aggregationStrategy=#testStrategy",
queueName, completionTimeout, completionSize).routeId("batchConsumer").startupOrder(10)
.log(LoggingLevel.DEBUG, "${body.size}")
.to("mock:batches");
}
});
context.start();
int messageCount = 100;
MockEndpoint mockBatches = getMockEndpoint("mock:batches");
mockBatches.expectedMessageCount(messageCount / completionSize);
template.sendBody("direct:in", generateStrings(messageCount));
mockBatches.assertIsSatisfied();
}
@Test
public void testConsumptionCompletionPredicate() throws Exception {
final String completionPredicate = "${body} contains 'done'";
final int completionTimeout = -1; // predicate-based only
final String queueName = getQueueName();
context.addRoutes(new TransactedSendHarness(queueName));
context.addRoutes(new RouteBuilder() {
public void configure() throws Exception {
fromF("sjms-batch:%s?completionTimeout=%s&completionPredicate=%s&aggregationStrategy=#testStrategy&eagerCheckCompletion=true",
queueName, completionTimeout, completionPredicate).routeId("batchConsumer").startupOrder(10)
.log(LoggingLevel.DEBUG, "${body.size}")
.to("mock:batches");
}
});
context.start();
MockEndpoint mockBatches = getMockEndpoint("mock:batches");
mockBatches.expectedMessageCount(2);
template.sendBody("direct:in", generateStrings(50));
template.sendBody("direct:in", "Message done");
template.sendBody("direct:in", generateStrings(50));
template.sendBody("direct:in", "Message done");
mockBatches.assertIsSatisfied();
}
@Test
public void testConsumptionCompletionTimeout() throws Exception {
final int completionTimeout = 2000;
final int completionSize = -1; // timeout-based only
final String queueName = getQueueName();
context.addRoutes(new TransactedSendHarness(queueName));
context.addRoutes(new RouteBuilder() {
public void configure() throws Exception {
fromF("sjms-batch:%s?completionTimeout=%s&completionSize=%s&aggregationStrategy=#testStrategy",
queueName, completionTimeout, completionSize).routeId("batchConsumer").startupOrder(10)
.to("mock:batches");
}
});
context.start();
int messageCount = 50;
assertTrue(messageCount < SjmsBatchEndpoint.DEFAULT_COMPLETION_SIZE);
MockEndpoint mockBatches = getMockEndpoint("mock:batches");
mockBatches.expectedMessageCount(1); // everything batched together
template.sendBody("direct:in", generateStrings(messageCount));
mockBatches.assertIsSatisfied();
assertFirstMessageBodyOfLength(mockBatches, messageCount);
}
@Test
public void testConsumptionCompletionInterval() throws Exception {
final int completionInterval = 2000;
final int completionSize = -1; // timeout-based only
final String queueName = getQueueName();
context.addRoutes(new TransactedSendHarness(queueName));
context.addRoutes(new RouteBuilder() {
public void configure() throws Exception {
fromF("sjms-batch:%s?completionInterval=%s&completionSize=%s&aggregationStrategy=#testStrategy",
queueName, completionInterval, completionSize).routeId("batchConsumer").startupOrder(10)
.to("mock:batches");
}
});
context.start();
int messageCount = 50;
assertTrue(messageCount < SjmsBatchEndpoint.DEFAULT_COMPLETION_SIZE);
MockEndpoint mockBatches = getMockEndpoint("mock:batches");
mockBatches.expectedMinimumMessageCount(1); // everything ought to be batched together but the interval may trigger in between and we get 2 etc
template.sendBody("direct:in", generateStrings(messageCount));
mockBatches.assertIsSatisfied();
}
@Test
public void testConsumptionSendEmptyMessageWhenIdle() throws Exception {
final int completionInterval = 2000;
final int completionSize = -1; // timeout-based only
final String queueName = getQueueName();
context.addRoutes(new TransactedSendHarness(queueName));
context.addRoutes(new RouteBuilder() {
public void configure() throws Exception {
fromF("sjms-batch:%s?completionInterval=%s&completionSize=%s&sendEmptyMessageWhenIdle=true&aggregationStrategy=#testStrategy",
queueName, completionInterval, completionSize).routeId("batchConsumer").startupOrder(10)
.to("mock:batches");
}
});
context.start();
int messageCount = 50;
assertTrue(messageCount < SjmsBatchEndpoint.DEFAULT_COMPLETION_SIZE);
MockEndpoint mockBatches = getMockEndpoint("mock:batches");
// trigger a couple of empty messages
mockBatches.expectedMinimumMessageCount(3);
template.sendBody("direct:in", generateStrings(messageCount));
mockBatches.assertIsSatisfied();
}
/**
* Checks whether multiple consumer endpoints can operate in parallel.
*/
@Test
public void testConsumptionMultipleConsumerEndpoints() throws Exception {
final int completionTimeout = 2000;
final int completionSize = 5;
final String queueName = getQueueName();
context.addRoutes(new RouteBuilder() {
public void configure() throws Exception {
from("direct:in")
.split().body()
.multicast()
.toF("sjms:%s", queueName + "A")
.toF("sjms:%s", queueName + "B")
.end();
fromF("sjms-batch:%s?completionTimeout=%s&completionSize=%s&aggregationStrategy=#testStrategy",
queueName + "A", completionTimeout, completionSize).routeId("batchConsumerA")
.to("mock:outA");
fromF("sjms-batch:%s?completionTimeout=%s&completionSize=%s&aggregationStrategy=#testStrategy",
queueName + "B", completionTimeout, completionSize).routeId("batchConsumerB")
.to("mock:outB");
}
});
context.start();
int messageCount = 5;
assertTrue(messageCount < SjmsBatchEndpoint.DEFAULT_COMPLETION_SIZE);
MockEndpoint mockOutA = getMockEndpoint("mock:outA");
mockOutA.expectedMessageCount(1); // everything batched together
MockEndpoint mockOutB = getMockEndpoint("mock:outB");
mockOutB.expectedMessageCount(1); // everything batched together
template.sendBody("direct:in", generateStrings(messageCount));
assertMockEndpointsSatisfied();
assertFirstMessageBodyOfLength(mockOutA, messageCount);
assertFirstMessageBodyOfLength(mockOutB, messageCount);
}
@Test
public void testConsumptionRollback() throws Exception {
final int completionTimeout = 2000;
final int completionSize = 5;
final String queueName = getQueueName();
context.addRoutes(new TransactedSendHarness(queueName));
context.addRoutes(new RouteBuilder() {
public void configure() throws Exception {
fromF("sjms-batch:%s?completionTimeout=%s&completionSize=%s&aggregationStrategy=#testStrategy",
queueName, completionTimeout, completionSize).routeId("batchConsumer").startupOrder(10)
.to("mock:batches");
}
});
context.start();
int messageCount = 5;
MockEndpoint mockBatches = getMockEndpoint("mock:batches");
// the first time around, the batch should throw an exception
mockBatches.whenExchangeReceived(1, new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
throw new RuntimeException("Boom!");
}
});
// so the batch should be processed twice due to redelivery
mockBatches.expectedMessageCount(2);
template.sendBody("direct:in", generateStrings(messageCount));
mockBatches.assertIsSatisfied();
}
@Test
public void testConsumptionBadSession() throws Exception {
final int messageCount = 5;
final int consumerCount = 1;
SjmsBatchComponent sb = (SjmsBatchComponent)context.getComponent("sjms-batch");
MockConnectionFactory cf = (MockConnectionFactory)sb.getConnectionFactory();
cf.returnBadSessionNTimes(2);
final String queueName = getQueueName();
context.addRoutes(new TransactedSendHarness(queueName));
context.addRoutes(new RouteBuilder() {
public void configure() throws Exception {
int completionTimeout = 1000;
int completionSize = 200;
// keepAliveDelay=300 is the key... it's a 300 millis delay between attempts to create a new session.
fromF("sjms-batch:%s?completionTimeout=%s&completionSize=%s&consumerCount=%s&aggregationStrategy=#testStrategy&keepAliveDelay=300",
queueName, completionTimeout, completionSize, consumerCount)
.routeId("batchConsumer").startupOrder(10).autoStartup(false)
.split(body())
.to("mock:split");
}
});
context.start();
MockEndpoint mockBefore = getMockEndpoint("mock:before");
mockBefore.setExpectedMessageCount(messageCount);
MockEndpoint mockSplit = getMockEndpoint("mock:split");
mockSplit.setExpectedMessageCount(messageCount);
LOG.info("Sending messages");
template.sendBody("direct:in", generateStrings(messageCount));
LOG.info("Send complete");
StopWatch stopWatch = new StopWatch();
context.startRoute("batchConsumer");
assertMockEndpointsSatisfied();
long time = stopWatch.stop();
}
private void assertFirstMessageBodyOfLength(MockEndpoint mockEndpoint, int expectedLength) {
Exchange exchange = mockEndpoint.getExchanges().get(0);
assertEquals(expectedLength, exchange.getIn().getBody(List.class).size());
}
private String getQueueName() {
SimpleDateFormat sdf = new SimpleDateFormat("yyMMddhhmmss");
return "sjms-batch-" + sdf.format(new Date());
}
private String[] generateStrings(int messageCount) {
String[] strings = new String[messageCount];
for (int i = 0; i < messageCount; i++) {
strings[i] = "message:" + i;
}
return strings;
}
}