/*
* Copyright 2015 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.kafka;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.beans.HasPropertyWithValue.hasProperty;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.hamcrest.core.IsEqual.equalTo;
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.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import org.hamcrest.CoreMatchers;
import org.junit.Ignore;
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.channel.interceptor.WireTap;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.kafka.support.KafkaHeaders;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.xd.dirt.integration.bus.Binding;
import org.springframework.xd.dirt.integration.bus.BusProperties;
import org.springframework.xd.dirt.integration.bus.MessageBus;
import org.springframework.xd.dirt.integration.bus.XdHeaders;
import org.springframework.xd.dirt.integration.kafka.KafkaMessageBus;
import kafka.admin.AdminUtils$;
/**
* @author Marius Bogoevici
*/
public class RawModeKafkaMessageBusTests extends KafkaMessageBusTests {
@Override
protected KafkaTestMessageBus createKafkaTestMessageBus() {
return new KafkaTestMessageBus(kafkaTestSupport, getCodec(), KafkaMessageBus.Mode.raw);
}
@Test
@Override
public void testPartitionedModuleJava() throws Exception {
MessageBus bus = getMessageBus();
Properties properties = new Properties();
properties.put("partitionKeyExtractorClass", "org.springframework.xd.dirt.integration.bus.kafka.RawKafkaPartitionTestSupport");
properties.put("partitionSelectorClass", "org.springframework.xd.dirt.integration.bus.kafka.RawKafkaPartitionTestSupport");
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());
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<>(new byte[]{(byte)0}));
output.send(new GenericMessage<>(new byte[]{(byte)1}));
output.send(new GenericMessage<>(new byte[]{(byte)2}));
Message<?> receive0 = input0.receive(1000);
assertNotNull(receive0);
Message<?> receive1 = input1.receive(1000);
assertNotNull(receive1);
Message<?> receive2 = input2.receive(1000);
assertNotNull(receive2);
assertThat(Arrays.asList(
((byte[]) receive0.getPayload())[0],
((byte[]) receive1.getPayload())[0],
((byte[]) receive2.getPayload())[0]),
containsInAnyOrder((byte)0, (byte)1, (byte)2));
bus.unbindConsumers("partJ.0");
bus.unbindProducers("partJ.0");
}
@Test
@Override
public void testPartitionedModuleSpEL() throws Exception {
MessageBus bus = getMessageBus();
Properties properties = new Properties();
properties.put("partitionKeyExpression", "payload[0]");
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<byte[]> message2 = MessageBuilder.withPayload(new byte[]{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<>(new byte[]{1}));
output.send(new GenericMessage<>(new byte[]{0}));
Message<?> receive0 = input0.receive(1000);
assertNotNull(receive0);
Message<?> receive1 = input1.receive(1000);
assertNotNull(receive1);
Message<?> receive2 = input2.receive(1000);
assertNotNull(receive2);
assertThat(Arrays.asList(
((byte[]) receive0.getPayload())[0],
((byte[]) receive1.getPayload())[0],
((byte[]) receive2.getPayload())[0]),
containsInAnyOrder((byte)0, (byte)1, (byte)2));
bus.unbindConsumers("part.0");
bus.unbindProducers("part.0");
}
@Test
@Override
@SuppressWarnings("unchecked")
public void testPartitioningWithSingleReceiver() throws Exception {
MessageBus bus = getMessageBus();
String topicName = "foo" + System.currentTimeMillis() + ".0";
try {
byte partitionCount = 4;
Properties properties = new Properties();
properties.put("partitionKeyExpression", "payload[0]");
properties.put("partitionSelectorExpression", "hashCode()");
properties.put(BusProperties.MIN_PARTITION_COUNT, Integer.toString(partitionCount));
DirectChannel moduleOutputChannel = new DirectChannel();
QueueChannel moduleInputChannel = new QueueChannel();
bus.bindProducer(topicName, moduleOutputChannel, properties);
bus.bindConsumer(topicName, moduleInputChannel, null);
int totalSent = 0;
for (byte i = 0; i < partitionCount; i++) {
for (byte j = 0; j < partitionCount + 1; j++ ) {
// the distribution is uneven across partitions, so that we can verify that the bus doesn't round robin
moduleOutputChannel.send(new GenericMessage<>(new byte[] { (byte) (i * partitionCount + j) }));
totalSent ++;
}
}
List<Message<byte[]>> receivedMessages = new ArrayList<>();
for (int i = 0; i < totalSent; i++) {
assertTrue(receivedMessages.add((Message<byte[]>) moduleInputChannel.receive(2000)));
}
assertThat(receivedMessages, hasSize(totalSent));
for (Message<byte[]> receivedMessage : receivedMessages) {
int expectedPartition = receivedMessage.getPayload()[0] % partitionCount;
assertThat(expectedPartition, equalTo(receivedMessage.getHeaders().get(KafkaHeaders.PARTITION_ID)));
}
}
finally {
bus.unbindConsumers(topicName);
bus.unbindProducers(topicName);
AdminUtils$.MODULE$.deleteTopic(kafkaTestSupport.getZkClient(),topicName);
}
}
@Test
@Override
public void createInboundPubSubBeforeOutboundPubSub() throws Exception {
MessageBus messageBus = getMessageBus();
DirectChannel moduleOutputChannel = new DirectChannel();
// Test pub/sub by emulating how StreamPlugin handles taps
DirectChannel tapChannel = new DirectChannel();
QueueChannel moduleInputChannel = new QueueChannel();
QueueChannel module2InputChannel = new QueueChannel();
QueueChannel module3InputChannel = new QueueChannel();
// Create the tap first
String fooTapName = messageBus.isCapable(MessageBus.Capability.DURABLE_PUBSUB) ? "foo.tap:baz.http" : "tap:baz.http";
messageBus.bindPubSubConsumer(fooTapName, module2InputChannel, null);
// Then create the stream
messageBus.bindProducer("baz.0", moduleOutputChannel, null);
messageBus.bindConsumer("baz.0", moduleInputChannel, null);
moduleOutputChannel.addInterceptor(new WireTap(tapChannel));
messageBus.bindPubSubProducer("tap:baz.http", tapChannel, null);
// Another new module is using tap as an input channel
String barTapName = messageBus.isCapable(MessageBus.Capability.DURABLE_PUBSUB) ? "bar.tap:baz.http" : "tap:baz.http";
messageBus.bindPubSubConsumer(barTapName, module3InputChannel, null);
Message<?> message = MessageBuilder.withPayload("foo".getBytes()).setHeader(MessageHeaders.CONTENT_TYPE, "foo/bar").build();
boolean success = false;
boolean retried = false;
while (!success) {
moduleOutputChannel.send(message);
Message<?> inbound = moduleInputChannel.receive(5000);
assertNotNull(inbound);
assertEquals("foo", new String((byte[])inbound.getPayload()));
Message<?> tapped1 = module2InputChannel.receive(5000);
Message<?> tapped2 = module3InputChannel.receive(5000);
if (tapped1 == null || tapped2 == null) {
// listener may not have started
assertFalse("Failed to receive tap after retry", retried);
retried = true;
continue;
}
success = true;
assertEquals("foo", new String((byte[]) tapped1.getPayload()));
assertNull(tapped1.getHeaders().get(XdHeaders.XD_ORIGINAL_CONTENT_TYPE));
assertEquals("foo", new String((byte[])tapped2.getPayload()));
}
// delete one tap stream is deleted
messageBus.unbindConsumer(barTapName, module3InputChannel);
Message<?> message2 = MessageBuilder.withPayload("bar".getBytes()).setHeader(MessageHeaders.CONTENT_TYPE, "foo/bar").build();
moduleOutputChannel.send(message2);
// other tap still receives messages
Message<?> tapped = module2InputChannel.receive(5000);
assertNotNull(tapped);
// Removed tap does not
assertNull(module3InputChannel.receive(1000));
// when other tap stream is deleted
messageBus.unbindConsumer(fooTapName, module2InputChannel);
// Clean up as StreamPlugin would
messageBus.unbindConsumer("baz.0", moduleInputChannel);
messageBus.unbindProducer("baz.0", moduleOutputChannel);
messageBus.unbindProducers("tap:baz.http");
assertTrue(getBindings(messageBus).isEmpty());
}
@Test
@Override
public void testSendAndReceive() throws Exception {
MessageBus messageBus = getMessageBus();
DirectChannel moduleOutputChannel = new DirectChannel();
QueueChannel moduleInputChannel = new QueueChannel();
messageBus.bindProducer("foo.0", moduleOutputChannel, null);
messageBus.bindConsumer("foo.0", moduleInputChannel, null);
Message<?> message = MessageBuilder.withPayload("foo".getBytes()).build();
// Let the consumer actually bind to the producer before sending a msg
busBindUnbindLatency();
moduleOutputChannel.send(message);
Message<?> inbound = moduleInputChannel.receive(5000);
assertNotNull(inbound);
assertEquals("foo", new String((byte[])inbound.getPayload()));
messageBus.unbindProducers("foo.0");
messageBus.unbindConsumers("foo.0");
}
// Ignored, since raw mode does not support headers
@Test
@Override
@Ignore
public void testSendAndReceiveNoOriginalContentType() throws Exception {
}
@Override
@Test
public void testSendAndReceivePubSub() throws Exception {
MessageBus messageBus = getMessageBus();
DirectChannel moduleOutputChannel = new DirectChannel();
// Test pub/sub by emulating how StreamPlugin handles taps
DirectChannel tapChannel = new DirectChannel();
QueueChannel moduleInputChannel = new QueueChannel();
QueueChannel module2InputChannel = new QueueChannel();
QueueChannel module3InputChannel = new QueueChannel();
messageBus.bindProducer("baz.0", moduleOutputChannel, null);
messageBus.bindConsumer("baz.0", moduleInputChannel, null);
moduleOutputChannel.addInterceptor(new WireTap(tapChannel));
messageBus.bindPubSubProducer("tap:baz.http", tapChannel, null);
// A new module is using the tap as an input channel
String fooTapName = messageBus.isCapable(MessageBus.Capability.DURABLE_PUBSUB) ? "foo.tap:baz.http" : "tap:baz.http";
messageBus.bindPubSubConsumer(fooTapName, module2InputChannel, null);
// Another new module is using tap as an input channel
String barTapName = messageBus.isCapable(MessageBus.Capability.DURABLE_PUBSUB) ? "bar.tap:baz.http" : "tap:baz.http";
messageBus.bindPubSubConsumer(barTapName, module3InputChannel, null);
Message<?> message = MessageBuilder.withPayload("foo".getBytes()).build();
boolean success = false;
boolean retried = false;
while (!success) {
moduleOutputChannel.send(message);
Message<?> inbound = moduleInputChannel.receive(5000);
assertNotNull(inbound);
assertEquals("foo", new String((byte[])inbound.getPayload()));
Message<?> tapped1 = module2InputChannel.receive(5000);
Message<?> tapped2 = module3InputChannel.receive(5000);
if (tapped1 == null || tapped2 == null) {
// listener may not have started
assertFalse("Failed to receive tap after retry", retried);
retried = true;
continue;
}
success = true;
assertEquals("foo", new String((byte[])tapped1.getPayload()));
assertEquals("foo", new String((byte[])tapped2.getPayload()));
}
// delete one tap stream is deleted
messageBus.unbindConsumer(barTapName, module3InputChannel);
Message<?> message2 = MessageBuilder.withPayload("bar".getBytes()).build();
moduleOutputChannel.send(message2);
// other tap still receives messages
Message<?> tapped = module2InputChannel.receive(5000);
assertNotNull(tapped);
// Removed tap does not
assertNull(module3InputChannel.receive(1000));
// when other tap stream is deleted
messageBus.unbindConsumer(fooTapName, module2InputChannel);
// Clean up as StreamPlugin would
messageBus.unbindConsumer("baz.0", moduleInputChannel);
messageBus.unbindProducer("baz.0", moduleOutputChannel);
messageBus.unbindProducers("tap:baz.http");
assertTrue(getBindings(messageBus).isEmpty());
}
@Override
@Test
@SuppressWarnings("unchecked")
public void testSendAndReceivePubSubWithMultipleConsumers() throws Exception {
MessageBus messageBus = getMessageBus();
DirectChannel quuxUpstreamModuleOutputChannel = new DirectChannel();
// Test pub/sub by emulating how StreamPlugin handles taps
DirectChannel tapChannel = new DirectChannel();
QueueChannel basDownstreamModuleInputChannel = new QueueChannel();
QueueChannel fooTapDownstreamInputChannel = new QueueChannel();
QueueChannel barTapDowstreamInput1Channel = new QueueChannel();
QueueChannel barTapDowstreamInput2Channel = new QueueChannel();
String originalTopic = "quux.0";
messageBus.bindProducer(originalTopic, quuxUpstreamModuleOutputChannel, null);
messageBus.bindConsumer(originalTopic, basDownstreamModuleInputChannel, null);
quuxUpstreamModuleOutputChannel.addInterceptor(new WireTap(tapChannel));
Properties quuxTapProducerProperties = new Properties();
// set the partition count to two, so that the tap has two partitions as well
// this will be necessary to robin messages across the competing consumers
quuxTapProducerProperties.setProperty(BusProperties.MIN_PARTITION_COUNT, "2");
messageBus.bindPubSubProducer("tap:quux.http", tapChannel, quuxTapProducerProperties);
// A new module is using the tap as an input channel
String fooTapName = messageBus.isCapable(MessageBus.Capability.DURABLE_PUBSUB) ? "foo.tap:quux.http" : "tap:quux.http";
messageBus.bindPubSubConsumer(fooTapName, fooTapDownstreamInputChannel, null);
// Another new module is using tap as an input channel
String barTapName = messageBus.isCapable(MessageBus.Capability.DURABLE_PUBSUB) ? "bar.tap:quux.http" : "tap:quux.http";
Properties barTap1Properties = new Properties();
barTap1Properties.setProperty(BusProperties.COUNT, "2");
barTap1Properties.setProperty(BusProperties.SEQUENCE, "1");
messageBus.bindPubSubConsumer(barTapName, barTapDowstreamInput1Channel, barTap1Properties);
Properties barTap2Properties = new Properties();
barTap2Properties.setProperty(BusProperties.COUNT, "2");
barTap2Properties.setProperty(BusProperties.SEQUENCE, "2");
messageBus.bindPubSubConsumer(barTapName, barTapDowstreamInput2Channel, barTap2Properties);
Message<?> message1 = MessageBuilder.withPayload("foo1".getBytes()).build();
Message<?> message2 = MessageBuilder.withPayload("foo2".getBytes()).build();
quuxUpstreamModuleOutputChannel.send(message1);
quuxUpstreamModuleOutputChannel.send(message2);
Message<byte[]> inbound = (Message<byte[]>) basDownstreamModuleInputChannel.receive(5000);
assertNotNull(inbound);
assertEquals("foo1", new String(inbound.getPayload()));
inbound = (Message<byte[]>) basDownstreamModuleInputChannel.receive(5000);
assertNotNull(inbound);
assertEquals("foo2", new String(inbound.getPayload()));
List<Message<?>> tappedFoo = new ArrayList<>();
tappedFoo.add(fooTapDownstreamInputChannel.receive(5000));
tappedFoo.add(fooTapDownstreamInputChannel.receive(5000));
Message<byte[]> tappedBar1 = (Message<byte[]>) barTapDowstreamInput1Channel.receive(5000);
Message<byte[]> tappedBar2 = (Message<byte[]>) barTapDowstreamInput2Channel.receive(5000);
assertThat(tappedFoo,
CoreMatchers.<Message<?>>hasItems(hasProperty("payload", equalTo("foo1".getBytes())),
hasProperty("payload", equalTo("foo2".getBytes()))));
assertEquals("foo1", new String(tappedBar1.getPayload()));
assertEquals("foo2", new String(tappedBar2.getPayload()));
// when other tap stream is deleted
messageBus.unbindConsumers(fooTapName);
// Clean up as StreamPlugin would
messageBus.unbindConsumers(originalTopic);
messageBus.unbindProducers(originalTopic);
messageBus.unbindProducers("tap:quux.http");
messageBus.unbindConsumers(barTapName);
assertTrue(getBindings(messageBus).isEmpty());
}
}