/*
* Copyright 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.
* 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.xd.dirt.integration.bus;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasProperty;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import org.hamcrest.CustomMatcher;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.springframework.amqp.utils.test.TestUtils;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
/**
* Tests for buses that support partitioning.
*
* @author Gary Russell
*/
abstract public class PartitionCapableBusTests extends BrokerBusTests {
@Test
public void testBadProperties() throws Exception {
MessageBus bus = getMessageBus();
Properties properties = new Properties();
properties.put("foo", "bar");
properties.put("baz", "qux");
DirectChannel output = new DirectChannel();
try {
bus.bindProducer("badprops.0", output, properties);
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage(), allOf(Matchers.containsString(bus.getClass().getSimpleName().replace("Test", "")
+ " does not support producer "),
containsString("foo"),
containsString("baz"),
containsString(" for badprops.0.")));
}
properties.remove("baz");
try {
bus.bindConsumer("badprops.0", output, properties);
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage(), equalTo(bus.getClass().getSimpleName().replace("Test", "")
+ " does not support consumer property: foo for badprops.0."));
}
}
@Test
public void testPartitionedModuleSpEL() throws Exception {
MessageBus bus = getMessageBus();
Properties properties = new Properties();
properties.put("partitionKeyExpression", "payload");
properties.put("partitionSelectorExpression", "hashCode()");
properties.put(BusProperties.NEXT_MODULE_COUNT, "3");
properties.put(BusProperties.NEXT_MODULE_CONCURRENCY, "2");
DirectChannel output = new DirectChannel();
output.setBeanName("test.output");
bus.bindProducer("part.0", output, properties);
@SuppressWarnings("unchecked")
List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.bindings", List.class);
assertEquals(1, bindings.size());
try {
AbstractEndpoint endpoint = bindings.get(0).getEndpoint();
assertThat(getEndpointRouting(endpoint), containsString("part.0-' + headers['partition']"));
}
catch (UnsupportedOperationException ignored) {
}
properties.clear();
properties.put("concurrency", "2");
properties.put("partitionIndex", "0");
properties.put("count","3");
QueueChannel input0 = new QueueChannel();
input0.setBeanName("test.input0S");
bus.bindConsumer("part.0", input0, properties);
properties.put("partitionIndex", "1");
QueueChannel input1 = new QueueChannel();
input1.setBeanName("test.input1S");
bus.bindConsumer("part.0", input1, properties);
properties.put("partitionIndex", "2");
QueueChannel input2 = new QueueChannel();
input2.setBeanName("test.input2S");
bus.bindConsumer("part.0", input2, properties);
Message<Integer> message2 = MessageBuilder.withPayload(2)
.setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, "foo")
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, 42)
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_SIZE, 43)
.setHeader("xdReplyChannel", "bar")
.build();
output.send(message2);
output.send(new GenericMessage<Integer>(1));
output.send(new GenericMessage<Integer>(0));
Message<?> receive0 = input0.receive(1000);
assertNotNull(receive0);
Message<?> receive1 = input1.receive(1000);
assertNotNull(receive1);
Message<?> receive2 = input2.receive(1000);
assertNotNull(receive2);
Matcher<Message<?>> fooMatcher = new CustomMatcher<Message<?>>("the message with 'foo' as its correlationId") {
@Override
public boolean matches(Object item) {
IntegrationMessageHeaderAccessor accessor = new IntegrationMessageHeaderAccessor((Message<?>) item);
boolean result = "foo".equals(accessor.getCorrelationId()) &&
42 == accessor.getSequenceNumber() &&
43 == accessor.getSequenceSize() &&
"bar".equals(accessor.getHeader("xdReplyChannel"));
return result;
}
};
if (usesExplicitRouting()) {
assertEquals(0, receive0.getPayload());
assertEquals(1, receive1.getPayload());
assertEquals(2, receive2.getPayload());
assertThat(receive2, fooMatcher);
}
else {
assertThat(Arrays.asList(
(Integer) receive0.getPayload(),
(Integer) receive1.getPayload(),
(Integer) receive2.getPayload()),
containsInAnyOrder(0, 1, 2));
@SuppressWarnings("unchecked")
Matcher<Iterable<? extends Message<?>>> containsOur3Messages = containsInAnyOrder(
fooMatcher,
hasProperty("payload", equalTo(0)),
hasProperty("payload", equalTo(1))
);
assertThat(
Arrays.asList(receive0, receive1, receive2),
containsOur3Messages);
}
bus.unbindConsumers("part.0");
bus.unbindProducers("part.0");
}
@Test
public void testPartitionedModuleJava() throws Exception {
MessageBus bus = getMessageBus();
Properties properties = new Properties();
properties.put("partitionKeyExtractorClass", "org.springframework.xd.dirt.integration.bus.PartitionTestSupport");
properties.put("partitionSelectorClass", "org.springframework.xd.dirt.integration.bus.PartitionTestSupport");
properties.put(BusProperties.NEXT_MODULE_COUNT, "3");
properties.put(BusProperties.NEXT_MODULE_CONCURRENCY, "2");
DirectChannel output = new DirectChannel();
output.setBeanName("test.output");
bus.bindProducer("partJ.0", output, properties);
@SuppressWarnings("unchecked")
List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.bindings", List.class);
assertEquals(1, bindings.size());
if (usesExplicitRouting()) {
AbstractEndpoint endpoint = bindings.get(0).getEndpoint();
assertThat(getEndpointRouting(endpoint), containsString("partJ.0-' + headers['partition']"));
}
properties.clear();
properties.put("concurrency", "2");
properties.put("count","3");
properties.put("partitionIndex", "0");
QueueChannel input0 = new QueueChannel();
input0.setBeanName("test.input0J");
bus.bindConsumer("partJ.0", input0, properties);
properties.put("partitionIndex", "1");
QueueChannel input1 = new QueueChannel();
input1.setBeanName("test.input1J");
bus.bindConsumer("partJ.0", input1, properties);
properties.put("partitionIndex", "2");
QueueChannel input2 = new QueueChannel();
input2.setBeanName("test.input2J");
bus.bindConsumer("partJ.0", input2, properties);
output.send(new GenericMessage<Integer>(2));
output.send(new GenericMessage<Integer>(1));
output.send(new GenericMessage<Integer>(0));
Message<?> receive0 = input0.receive(1000);
assertNotNull(receive0);
Message<?> receive1 = input1.receive(1000);
assertNotNull(receive1);
Message<?> receive2 = input2.receive(1000);
assertNotNull(receive2);
if (usesExplicitRouting()) {
assertEquals(0, receive0.getPayload());
assertEquals(1, receive1.getPayload());
assertEquals(2, receive2.getPayload());
}
else {
assertThat(Arrays.asList(
(Integer) receive0.getPayload(),
(Integer) receive1.getPayload(),
(Integer) receive2.getPayload()),
containsInAnyOrder(0, 1, 2));
}
bus.unbindConsumers("partJ.0");
bus.unbindProducers("partJ.0");
}
@Test
public void testPartitioningWithSingleReceiver() throws Exception {
MessageBus bus = getMessageBus();
Properties properties = new Properties();
properties.put("partitionKeyExtractorClass", "org.springframework.xd.dirt.integration.bus.PartitionTestSupport");
properties.put("partitionSelectorClass", "org.springframework.xd.dirt.integration.bus.PartitionTestSupport");
try {
DirectChannel output = new DirectChannel();
output.setBeanName("test.output");
bus.bindProducer("partK.0", output, properties);
fail();
}
catch (IllegalArgumentException e) {
assertThat(e.getMessage(), Matchers.equalTo(bus.getClass().getSimpleName().replace("Test", "")
+ " requires partitioned data to be sent to a module having 'count' > 1 for 'partK.0'"));
}
}
/**
* Implementations should return whether the bus under test uses "explicit" routing (e.g. Rabbit)
* whereby XD is responsible for assigning a partition and knows which exact consumer will receive the
* message (i.e. honor "partitionIndex") or "implicit" routing (e.g. Kafka) whereby the only guarantee
* is that messages will be spread, but we don't control exactly which consumer gets which message.
*/
protected abstract boolean usesExplicitRouting();
/**
* For implementations that rely on explicit routing, return the routing expression.
*/
protected String getEndpointRouting(AbstractEndpoint endpoint) {
throw new UnsupportedOperationException();
}
/**
* For implementations that rely on explicit routing, return the routing expression.
*/
protected String getPubSubEndpointRouting(AbstractEndpoint endpoint) {
throw new UnsupportedOperationException();
}
}