From c3758b9dc0d0ccebbf162a7f75affb84a1b08ff0 Mon Sep 17 00:00:00 2001 From: Mark Fisher Date: Mon, 21 Dec 2015 15:48:48 -0500 Subject: [PATCH] Binder simplification * removed pub/sub methods from binder * consumer group is now a parameter of the remaining bindConsumer method * remove DynamicProducer from Binder * Move logic to create the dynamic channel to the channel resolver. * Return bindings from bind methods and use them for unbinding * Suffix for dlq Move All Rabbit Binder CleanUp to Test Bindera More RabbitMQ Binder Test Cleanup Clean up declarations for remaining tests. removed BinderUtils use Redis ZSET for consumer groups copyright dates AutoBindDLQ: Single DLQ Per Group When Partitioned Configure a single DLQ for each group for all partitions. Add DLX Exchange binding for each original queue routing key, including the partition. Fix DLQ Binding (Producer Side) Option was not allowed and the routing key was wrong. Add test to verify producers can be bound before consumers. `autoBindDLQ` must be set (or reset) on both sides for success. --- .../kafka/KafkaMessageChannelBinder.java | 163 ++---- .../stream/binder/kafka/KafkaBinderTests.java | 70 +-- .../binder/kafka/RawModeKafkaBinderTests.java | 156 ++--- .../binder/rabbit/RabbitBindingCleaner.java | 9 +- .../rabbit/RabbitMessageChannelBinder.java | 283 +++------ ...ueueConnectionFactoryIntegrationTests.java | 6 +- .../rabbit/RabbitBinderCleanerTests.java | 14 +- .../binder/rabbit/RabbitBinderTests.java | 543 ++++++++---------- .../binder/rabbit/RabbitTestBinder.java | 91 ++- .../integration/RabbitBinderModuleTests.java | 6 +- .../redis/RedisMessageChannelBinder.java | 254 +++----- .../stream/binder/redis/RedisBinderTests.java | 236 ++------ .../stream/binder/redis/RedisTestBinder.java | 6 +- .../stream/binder/AbstractBinderTests.java | 196 +------ .../stream/binder/AbstractTestBinder.java | 80 +-- .../stream/binder/BrokerBinderTests.java | 85 +-- .../binder/PartitionCapableBinderTests.java | 141 ++--- .../MessageChannelBinderSupportTests.java | 26 +- .../TwoKafkaBindersApplicationTest.java | 6 +- .../RabbitAndRedisBinderApplicationTests.java | 13 +- .../stream/test/binder/TestSupportBinder.java | 69 +-- .../AbstractBindingPropertiesAccessor.java | 11 +- .../cloud/stream/binder/Binder.java | 102 +--- .../stream/binder/BinderPropertyKeys.java | 8 +- .../cloud/stream/binder/BinderUtils.java | 59 -- .../cloud/stream/binder/Binding.java | 110 +++- .../binder/MessageChannelBinderSupport.java | 311 ++-------- .../binding/BinderAwareChannelResolver.java | 70 ++- .../stream/binding/ChannelBindingService.java | 56 +- ...terfaceBindingTestsWithBindingTargets.java | 7 +- ...raryInterfaceBindingTestsWithDefaults.java | 7 +- .../BinderAwareChannelResolverTests.java | 66 +-- .../binder/InputOutputBindingOrderTest.java | 6 +- ...ocessorBindingTestsWithBindingTargets.java | 5 +- .../ProcessorBindingTestsWithDefaults.java | 5 +- ...rBindingTestsWithPubSubBindingTargets.java | 71 --- .../stream/binder/SinkBindingPubSubTests.java | 70 --- .../SinkBindingTestsWithBindingTargets.java | 5 +- .../binder/SinkBindingTestsWithDefaults.java | 5 +- .../local/LocalMessageChannelBinder.java | 198 +------ .../stream/binder/stub1/StubBinder1.java | 70 +-- .../stub1/StubBinder1Configuration.java | 4 +- .../stream/binder/stub2/StubBinder2.java | 73 +-- .../stub2/StubBinder2ConfigurationA.java | 4 +- .../binding/ChannelBindingServiceTests.java | 26 +- .../partitioning/PartitionedConsumerTest.java | 8 +- 46 files changed, 1052 insertions(+), 2758 deletions(-) delete mode 100644 spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderUtils.java delete mode 100644 spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithPubSubBindingTargets.java delete mode 100644 spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingPubSubTests.java diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaMessageChannelBinder.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaMessageChannelBinder.java index 7e689b392..249f7f13c 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaMessageChannelBinder.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/main/java/org/springframework/cloud/stream/binder/kafka/KafkaMessageChannelBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-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. @@ -117,11 +117,13 @@ import scala.collection.Seq; * that better be greater than number of containers * * + * * @author Eric Bottard * @author Marius Bogoevici * @author Ilayaperumal Gopinathan * @author David Turanski * @author Gary Russell + * @author Mark Fisher */ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { @@ -184,12 +186,6 @@ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { KafkaMessageChannelBinder.COMPRESSION_CODEC, })); - /** - * The consumer group to use when achieving point to point semantics (that consumer group name is static and hence - * shared by all containers). - */ - private static final String POINT_TO_POINT_SEMANTICS_CONSUMER_GROUP = "springXD"; - private static final Set KAFKA_CONSUMER_PROPERTIES = new SetBuilder() .add(BinderPropertyKeys.MIN_PARTITION_COUNT) .build(); @@ -210,25 +206,12 @@ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { .add(BinderPropertyKeys.MIN_PARTITION_COUNT) .build(); - /** - * Basic + concurrency. - */ - private static final Set SUPPORTED_NAMED_CONSUMER_PROPERTIES = new SetBuilder() - .addAll(CONSUMER_STANDARD_PROPERTIES) - .build(); - - private static final Set SUPPORTED_NAMED_PRODUCER_PROPERTIES = new SetBuilder() - .addAll(PRODUCER_STANDARD_PROPERTIES) - .addAll(PRODUCER_BATCHING_BASIC_PROPERTIES) - .build(); - /** * Partitioning + kafka producer properties. */ private static final Set SUPPORTED_PRODUCER_PROPERTIES = new SetBuilder() .addAll(PRODUCER_PARTITIONING_PROPERTIES) .addAll(PRODUCER_STANDARD_PROPERTIES) - .add(BinderPropertyKeys.DIRECT_BINDING_ALLOWED) .addAll(KAFKA_PRODUCER_PROPERTIES) .addAll(PRODUCER_BATCHING_BASIC_PROPERTIES) .addAll(PRODUCER_COMPRESSION_PROPERTIES) @@ -246,6 +229,7 @@ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { private final String zkAddress; // -------- Default values for properties ------- + private int defaultReplicationFactor = 1; private String defaultCompressionCodec = DEFAULT_COMPRESSION_CODEC; @@ -306,7 +290,6 @@ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { else { this.headersToMap = BinderHeaders.STANDARD_HEADERS; } - } public void setOffsetStoreTopic(String offsetStoreTopic) { @@ -333,7 +316,6 @@ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { this.offsetStoreMaxFetchSize = offsetStoreMaxFetchSize; } - public void setOffsetUpdateTimeWindow(int offsetUpdateTimeWindow) { this.offsetUpdateTimeWindow = offsetUpdateTimeWindow; } @@ -460,99 +442,66 @@ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { } @Override - public void bindConsumer(String name, final MessageChannel moduleInputChannel, Properties properties) { - // Point-to-point consumers reset at the earliest time, which allows them to catch up with all messages - createKafkaConsumer(name, moduleInputChannel, properties, POINT_TO_POINT_SEMANTICS_CONSUMER_GROUP, - OffsetRequest.EarliestTime()); - bindExistingProducerDirectlyIfPossible(name, moduleInputChannel); - } - - @Override - public void bindPubSubConsumer(String name, MessageChannel inputChannel, String group, Properties properties) { + protected Binding doBindConsumer(String name, String group, MessageChannel inputChannel, Properties properties) { // If the caller provides a group, use it; otherwise // usage of a different consumer group each time achieves pub-sub // but multiple instances of this binding will each get all messages // PubSub consumers reset at the latest time, which allows them to receive only messages sent after // they've been bound String consumerGroup = group == null ? UUID.randomUUID().toString() : group; - createKafkaConsumer(name, inputChannel, properties, consumerGroup, OffsetRequest.LatestTime()); + return createKafkaConsumer(name, inputChannel, properties, consumerGroup, OffsetRequest.LatestTime()); } @Override - public void bindProducer(final String name, MessageChannel moduleOutputChannel, Properties properties) { - + public Binding bindProducer(final String name, MessageChannel moduleOutputChannel, Properties properties) { Assert.isInstanceOf(SubscribableChannel.class, moduleOutputChannel); KafkaPropertiesAccessor producerPropertiesAccessor = new KafkaPropertiesAccessor(properties); - if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX)) { - validateProducerProperties(name, properties, SUPPORTED_NAMED_PRODUCER_PROPERTIES); - } - else { - validateProducerProperties(name, properties, SUPPORTED_PRODUCER_PROPERTIES); - } - if (!bindNewProducerDirectlyIfPossible(name, (SubscribableChannel) moduleOutputChannel, - producerPropertiesAccessor)) { - if (logger.isInfoEnabled()) { - logger.info("Using kafka topic for outbound: " + name); - } - - final String topicName = escapeTopicName(name); - - int numPartitions = producerPropertiesAccessor.getNumberOfKafkaPartitionsForProducer(); - - Collection partitions = ensureTopicCreated(topicName, numPartitions, defaultReplicationFactor); - - ProducerMetadata producerMetadata = new ProducerMetadata<>( - topicName, byte[].class, byte[].class, BYTE_ARRAY_SERIALIZER, BYTE_ARRAY_SERIALIZER); - producerMetadata.setCompressionType(ProducerMetadata.CompressionType.valueOf( - producerPropertiesAccessor.getCompressionCodec(this.defaultCompressionCodec))); - producerMetadata.setBatchBytes(producerPropertiesAccessor.getBatchSize(this.defaultBatchSize)); - Properties additionalProps = new Properties(); - additionalProps.put(ProducerConfig.ACKS_CONFIG, - String.valueOf(producerPropertiesAccessor.getRequiredAcks(this - .defaultRequiredAcks))); - additionalProps.put(ProducerConfig.LINGER_MS_CONFIG, - String.valueOf(producerPropertiesAccessor.getBatchTimeout(this - .defaultBatchTimeout))); - ProducerFactoryBean producerFB = - new ProducerFactoryBean<>(producerMetadata, brokers, additionalProps); - - try { - final ProducerConfiguration producerConfiguration = new ProducerConfiguration<>( - producerMetadata, producerFB.getObject()); - - MessageHandler handler = new SendingHandler(topicName, producerPropertiesAccessor, - partitions.size(), producerConfiguration); - EventDrivenConsumer consumer = new EventDrivenConsumer((SubscribableChannel) moduleOutputChannel, - handler); - consumer.setBeanFactory(this.getBeanFactory()); - consumer.setBeanName("outbound." + name); - consumer.afterPropertiesSet(); - Binding producerBinding = Binding.forProducer(name, moduleOutputChannel, consumer, - producerPropertiesAccessor); - addBinding(producerBinding); - producerBinding.start(); - } - catch (Exception e) { - throw new RuntimeException(e); - } - + validateProducerProperties(name, properties, SUPPORTED_PRODUCER_PROPERTIES); + if (logger.isInfoEnabled()) { + logger.info("Using kafka topic for outbound: " + name); } - } + final String topicName = escapeTopicName(name); - @Override - public void bindPubSubProducer(String name, MessageChannel outputChannel, Properties properties) { - bindProducer(name, outputChannel, properties); - } + int numPartitions = producerPropertiesAccessor.getNumberOfKafkaPartitionsForProducer(); - @Override - public void bindRequestor(String name, MessageChannel requests, MessageChannel replies, Properties properties) { - throw new UnsupportedOperationException("requestor binding is not supported by this binder"); - } + Collection partitions = ensureTopicCreated(topicName, numPartitions, defaultReplicationFactor); - @Override - public void bindReplier(String name, MessageChannel requests, MessageChannel replies, Properties properties) { - throw new UnsupportedOperationException("replier binding is not supported by this binder"); + ProducerMetadata producerMetadata = new ProducerMetadata<>( + topicName, byte[].class, byte[].class, BYTE_ARRAY_SERIALIZER, BYTE_ARRAY_SERIALIZER); + producerMetadata.setCompressionType(ProducerMetadata.CompressionType.valueOf( + producerPropertiesAccessor.getCompressionCodec(this.defaultCompressionCodec))); + producerMetadata.setBatchBytes(producerPropertiesAccessor.getBatchSize(this.defaultBatchSize)); + Properties additionalProps = new Properties(); + additionalProps.put(ProducerConfig.ACKS_CONFIG, + String.valueOf(producerPropertiesAccessor.getRequiredAcks(this + .defaultRequiredAcks))); + additionalProps.put(ProducerConfig.LINGER_MS_CONFIG, + String.valueOf(producerPropertiesAccessor.getBatchTimeout(this + .defaultBatchTimeout))); + ProducerFactoryBean producerFB = + new ProducerFactoryBean<>(producerMetadata, brokers, additionalProps); + + try { + final ProducerConfiguration producerConfiguration = new ProducerConfiguration<>( + producerMetadata, producerFB.getObject()); + + MessageHandler handler = new SendingHandler(topicName, producerPropertiesAccessor, + partitions.size(), producerConfiguration); + EventDrivenConsumer consumer = new EventDrivenConsumer((SubscribableChannel) moduleOutputChannel, + handler); + consumer.setBeanFactory(this.getBeanFactory()); + consumer.setBeanName("outbound." + name); + consumer.afterPropertiesSet(); + Binding producerBinding = Binding.forProducer(name, moduleOutputChannel, consumer, + producerPropertiesAccessor); + addBinding(producerBinding); + producerBinding.start(); + return producerBinding; + } + catch (Exception e) { + throw new RuntimeException(e); + } } /** @@ -610,15 +559,10 @@ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { } } - private void createKafkaConsumer(String name, final MessageChannel moduleInputChannel, Properties properties, + private Binding createKafkaConsumer(String name, final MessageChannel moduleInputChannel, Properties properties, String group, long referencePoint) { - if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX)) { - validateConsumerProperties(name, properties, SUPPORTED_NAMED_CONSUMER_PROPERTIES); - } - else { - validateConsumerProperties(name, properties, SUPPORTED_CONSUMER_PROPERTIES); - } + validateConsumerProperties(groupedName(name, group), properties, SUPPORTED_CONSUMER_PROPERTIES); KafkaPropertiesAccessor accessor = new KafkaPropertiesAccessor(properties); int maxConcurrency = accessor.getConcurrency(defaultConcurrency); @@ -702,12 +646,13 @@ public class KafkaMessageChannelBinder extends MessageChannelBinderSupport { super.doStop(); } }; - edc.setBeanName("inbound." + name); + String groupedName = groupedName(name, group); + edc.setBeanName("inbound." + groupedName); - Binding consumerBinding = Binding.forConsumer(name, edc, moduleInputChannel, accessor); + Binding consumerBinding = Binding.forConsumer(name, group, edc, moduleInputChannel, accessor); addBinding(consumerBinding); consumerBinding.start(); - + return consumerBinding; } public KafkaMessageListenerContainer createMessageListenerContainer(Properties properties, String group, diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderTests.java index c092ad7cd..0873b027e 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/KafkaBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-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. @@ -29,13 +29,12 @@ import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; -import kafka.api.OffsetRequest; import org.junit.ClassRule; -import org.junit.Ignore; import org.junit.Test; import org.springframework.cloud.stream.binder.Binder; import org.springframework.cloud.stream.binder.BinderPropertyKeys; +import org.springframework.cloud.stream.binder.Binding; import org.springframework.cloud.stream.binder.PartitionCapableBinderTests; import org.springframework.cloud.stream.binder.Spy; import org.springframework.cloud.stream.test.junit.kafka.KafkaTestSupport; @@ -48,12 +47,15 @@ import org.springframework.integration.kafka.listener.MessageListener; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; +import kafka.api.OffsetRequest; + /** * Integration tests for the {@link KafkaMessageChannelBinder}. * * @author Eric Bottard * @author Marius Bogoevici + * @author Mark Fisher */ public class KafkaBinderTests extends PartitionCapableBinderTests { @@ -137,8 +139,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { if (codec != null) { props.put(KafkaMessageChannelBinder.COMPRESSION_CODEC, codec); } - binder.bindProducer("foo.0", moduleOutputChannel, props); - binder.bindConsumer("foo.0", moduleInputChannel, null); + Binding producerBinding = binder.bindProducer("foo.0", moduleOutputChannel, props); + Binding consumerBinding = binder.bindConsumer("foo.0", "test", moduleInputChannel, null); Message message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build(); // Let the consumer actually bind to the producer before sending a msg binderBindUnbindLatency(); @@ -146,8 +148,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Message inbound = moduleInputChannel.receive(2000); assertNotNull(inbound); assertArrayEquals(ratherBigPayload, (byte[]) inbound.getPayload()); - binder.unbindProducers("foo.0"); - binder.unbindConsumers("foo.0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } } @@ -166,8 +168,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Properties consumerProperties = new Properties(); consumerProperties.put(BinderPropertyKeys.MIN_PARTITION_COUNT, "10"); long uniqueBindingId = System.currentTimeMillis(); - binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties); - binder.bindConsumer("foo" + uniqueBindingId + ".0", moduleInputChannel, consumerProperties); + Binding producerBinding = binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties); + Binding consumerBinding = binder.bindConsumer("foo" + uniqueBindingId + ".0", null, moduleInputChannel, consumerProperties); Message message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build(); // Let the consumer actually bind to the producer before sending a msg binderBindUnbindLatency(); @@ -178,8 +180,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Collection partitions = binder.getCoreBinder().getConnectionFactory().getPartitions( "foo" + uniqueBindingId + ".0"); assertThat(partitions, hasSize(10)); - binder.unbindProducers("foo" + uniqueBindingId + ".0"); - binder.unbindConsumers("foo" + uniqueBindingId + ".0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } @Test @@ -199,8 +201,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { consumerProps.put(BinderPropertyKeys.MIN_PARTITION_COUNT, "5"); consumerProps.put(BinderPropertyKeys.CONCURRENCY, "6"); long uniqueBindingId = System.currentTimeMillis(); - binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProps); - binder.bindConsumer("foo" + uniqueBindingId + ".0", moduleInputChannel, consumerProps); + Binding producerBinding = binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProps); + Binding consumerBinding = binder.bindConsumer("foo" + uniqueBindingId + ".0", null, moduleInputChannel, consumerProps); Message message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build(); // Let the consumer actually bind to the producer before sending a msg binderBindUnbindLatency(); @@ -211,8 +213,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Collection partitions = binder.getCoreBinder().getConnectionFactory().getPartitions( "foo" + uniqueBindingId + ".0"); assertThat(partitions, hasSize(6)); - binder.unbindProducers("foo" + uniqueBindingId + ".0"); - binder.unbindConsumers("foo" + uniqueBindingId + ".0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } @Test @@ -231,8 +233,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { consumerProps.put(BinderPropertyKeys.MIN_PARTITION_COUNT, "6"); consumerProps.put(BinderPropertyKeys.CONCURRENCY, "5"); long uniqueBindingId = System.currentTimeMillis(); - binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProps); - binder.bindConsumer("foo" + uniqueBindingId + ".0", moduleInputChannel, consumerProps); + Binding producerBinding = binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProps); + Binding consumerBinding = binder.bindConsumer("foo" + uniqueBindingId + ".0", null, moduleInputChannel, consumerProps); Message message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build(); // Let the consumer actually bind to the producer before sending a msg binderBindUnbindLatency(); @@ -243,8 +245,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Collection partitions = binder.getCoreBinder().getConnectionFactory().getPartitions( "foo" + uniqueBindingId + ".0"); assertThat(partitions, hasSize(6)); - binder.unbindProducers("foo" + uniqueBindingId + ".0"); - binder.unbindConsumers("foo" + uniqueBindingId + ".0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } @Test @@ -263,8 +265,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Properties consumerProperties = new Properties(); consumerProperties.put(BinderPropertyKeys.MIN_PARTITION_COUNT, "3"); long uniqueBindingId = System.currentTimeMillis(); - binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties); - binder.bindConsumer("foo" + uniqueBindingId + ".0", moduleInputChannel, consumerProperties); + Binding producerBinding = binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties); + Binding consumerBinding = binder.bindConsumer("foo" + uniqueBindingId + ".0", null, moduleInputChannel, consumerProperties); Message message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build(); // Let the consumer actually bind to the producer before sending a msg binderBindUnbindLatency(); @@ -275,8 +277,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Collection partitions = binder.getCoreBinder().getConnectionFactory().getPartitions( "foo" + uniqueBindingId + ".0"); assertThat(partitions, hasSize(5)); - binder.unbindProducers("foo" + uniqueBindingId + ".0"); - binder.unbindConsumers("foo" + uniqueBindingId + ".0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } @Test @@ -295,8 +297,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Properties consumerProperties = new Properties(); consumerProperties.put(BinderPropertyKeys.MIN_PARTITION_COUNT, "5"); long uniqueBindingId = System.currentTimeMillis(); - binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties); - binder.bindConsumer("foo" + uniqueBindingId + ".0", moduleInputChannel, consumerProperties); + Binding producerBinding = binder.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties); + Binding consumerBinding = binder.bindConsumer("foo" + uniqueBindingId + ".0", null, moduleInputChannel, consumerProperties); Message message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build(); // Let the consumer actually bind to the producer before sending a msg binderBindUnbindLatency(); @@ -307,22 +309,8 @@ public class KafkaBinderTests extends PartitionCapableBinderTests { Collection partitions = binder.getCoreBinder().getConnectionFactory().getPartitions( "foo" + uniqueBindingId + ".0"); assertThat(partitions, hasSize(5)); - binder.unbindProducers("foo" + uniqueBindingId + ".0"); - binder.unbindConsumers("foo" + uniqueBindingId + ".0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } - @Override @Ignore("https://github.com/spring-cloud/spring-cloud-stream/issues/243") - public void testSendAndReceivePubSub() throws Exception { - } - - @Override @Ignore("https://github.com/spring-cloud/spring-cloud-stream/issues/243") - public void createInboundPubSubBeforeOutboundPubSub() throws Exception { - } - - @Test - @Ignore("Kafka binder does not support direct binding") - @Override - public void testDirectBinding() throws Exception { - - } } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/RawModeKafkaBinderTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/RawModeKafkaBinderTests.java index 38d0cd092..394c0f06d 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/RawModeKafkaBinderTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-kafka/src/test/java/org/springframework/cloud/stream/binder/kafka/RawModeKafkaBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -33,28 +33,24 @@ import org.junit.Ignore; import org.junit.Test; import org.springframework.cloud.stream.binder.Binder; -import org.springframework.cloud.stream.binder.BinderHeaders; import org.springframework.cloud.stream.binder.BinderPropertyKeys; import org.springframework.cloud.stream.binder.Binding; import org.springframework.cloud.stream.binder.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.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.support.GenericMessage; /** * @author Marius Bogoevici * @author David Turanski * @author Gary Russell + * @author Mark Fisher */ - -@Ignore public class RawModeKafkaBinderTests extends KafkaBinderTests { @Override @@ -74,9 +70,9 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { DirectChannel output = new DirectChannel(); output.setBeanName("test.output"); - binder.bindProducer("partJ.0", output, properties); + Binding outputBinding = binder.bindProducer("partJ.0", output, properties); @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + List> bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); assertEquals(1, bindings.size()); properties.clear(); @@ -85,15 +81,15 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { properties.put("partitionIndex", "0"); QueueChannel input0 = new QueueChannel(); input0.setBeanName("test.input0J"); - binder.bindConsumer("partJ.0", input0, properties); + Binding input0Binding = binder.bindConsumer("partJ.0", "test", input0, properties); properties.put("partitionIndex", "1"); QueueChannel input1 = new QueueChannel(); input1.setBeanName("test.input1J"); - binder.bindConsumer("partJ.0", input1, properties); + Binding input1Binding = binder.bindConsumer("partJ.0", "test", input1, properties); properties.put("partitionIndex", "2"); QueueChannel input2 = new QueueChannel(); input2.setBeanName("test.input2J"); - binder.bindConsumer("partJ.0", input2, properties); + Binding input2Binding = binder.bindConsumer("partJ.0", "test", input2, properties); output.send(new GenericMessage<>(new byte[]{(byte)0})); output.send(new GenericMessage<>(new byte[]{(byte)1})); @@ -112,8 +108,10 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { ((byte[]) receive2.getPayload())[0]), containsInAnyOrder((byte)0, (byte)1, (byte)2)); - binder.unbindConsumers("partJ.0"); - binder.unbindProducers("partJ.0"); + binder.unbind(input0Binding); + binder.unbind(input1Binding); + binder.unbind(input2Binding); + binder.unbind(outputBinding); } @Test @@ -128,9 +126,9 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { DirectChannel output = new DirectChannel(); output.setBeanName("test.output"); - binder.bindProducer("part.0", output, properties); + Binding outputBinding = binder.bindProducer("part.0", output, properties); @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + List> bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); assertEquals(1, bindings.size()); try { AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); @@ -146,15 +144,15 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { properties.put("count","3"); QueueChannel input0 = new QueueChannel(); input0.setBeanName("test.input0S"); - binder.bindConsumer("part.0", input0, properties); + Binding input0Binding = binder.bindConsumer("part.0", "test", input0, properties); properties.put("partitionIndex", "1"); QueueChannel input1 = new QueueChannel(); input1.setBeanName("test.input1S"); - binder.bindConsumer("part.0", input1, properties); + Binding input1Binding = binder.bindConsumer("part.0", "test", input1, properties); properties.put("partitionIndex", "2"); QueueChannel input2 = new QueueChannel(); input2.setBeanName("test.input2S"); - binder.bindConsumer("part.0", input2, properties); + Binding input2Binding = binder.bindConsumer("part.0", "test", input2, properties); Message message2 = MessageBuilder.withPayload(new byte[]{2}) .setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, "foo") @@ -180,74 +178,10 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { ((byte[]) receive2.getPayload())[0]), containsInAnyOrder((byte)0, (byte)1, (byte)2)); - binder.unbindConsumers("part.0"); - binder.unbindProducers("part.0"); - } - - @Test - @Override - @Ignore - public void createInboundPubSubBeforeOutboundPubSub() throws Exception { - Binder binder = getBinder(); - 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 = "baz.0"; - binder.bindPubSubConsumer(fooTapName, module2InputChannel, null, null); - - // Then create the stream - binder.bindProducer("baz.0", moduleOutputChannel, null); - binder.bindConsumer("baz.0", moduleInputChannel, null); - moduleOutputChannel.addInterceptor(new WireTap(tapChannel)); - binder.bindPubSubProducer(fooTapName, tapChannel, null); - - // Another new module is using tap as an input channel - String barTapName = "baz.0"; - binder.bindPubSubConsumer(barTapName, module3InputChannel, null, 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(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); - assertEquals("foo", new String((byte[])tapped2.getPayload())); - } - // delete one tap stream is deleted - binder.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 - binder.unbindConsumer(fooTapName, module2InputChannel); - // Clean up as StreamPlugin would - binder.unbindConsumer("baz.0", moduleInputChannel); - binder.unbindProducers("baz.0"); - binder.unbindConsumers("baz.0"); - assertTrue(getBindings(binder).isEmpty()); + binder.unbind(input0Binding); + binder.unbind(input1Binding); + binder.unbind(input2Binding); + binder.unbind(outputBinding); } @Test @@ -256,8 +190,8 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { Binder binder = getBinder(); DirectChannel moduleOutputChannel = new DirectChannel(); QueueChannel moduleInputChannel = new QueueChannel(); - binder.bindProducer("foo.0", moduleOutputChannel, null); - binder.bindConsumer("foo.0", moduleInputChannel, null); + Binding producerBinding = binder.bindProducer("foo.0", moduleOutputChannel, null); + Binding consumerBinding = binder.bindConsumer("foo.0", "test", moduleInputChannel, null); Message message = MessageBuilder.withPayload("foo".getBytes()).build(); // Let the consumer actually bind to the producer before sending a msg binderBindUnbindLatency(); @@ -265,8 +199,8 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { Message inbound = moduleInputChannel.receive(5000); assertNotNull(inbound); assertEquals("foo", new String((byte[])inbound.getPayload())); - binder.unbindProducers("foo.0"); - binder.unbindConsumers("foo.0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } // Ignored, since raw mode does not support headers @@ -277,32 +211,29 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { } - @Override @Test - public void testSendAndReceivePubSub() throws Exception { + public void testSendAndReceiveWithExplicitConsumerGroup() { Binder binder = getBinder(); DirectChannel moduleOutputChannel = new DirectChannel(); // Test pub/sub by emulating how StreamPlugin handles taps - DirectChannel tapChannel = new DirectChannel(); - QueueChannel moduleInputChannel = new QueueChannel(); + QueueChannel module1InputChannel = new QueueChannel(); QueueChannel module2InputChannel = new QueueChannel(); QueueChannel module3InputChannel = new QueueChannel(); - binder.bindProducer("baz.0", moduleOutputChannel, null); - binder.bindConsumer("baz.0", moduleInputChannel, null); - moduleOutputChannel.addInterceptor(new WireTap(tapChannel)); + Binding producerBinding = binder.bindProducer("baz.0", moduleOutputChannel, null); + Binding input1Binding = binder.bindConsumer("baz.0", "test", module1InputChannel, null); // A new module is using the tap as an input channel String fooTapName = "baz.0"; - binder.bindPubSubProducer(fooTapName, tapChannel, null); - binder.bindPubSubConsumer(fooTapName, module2InputChannel, null, null); + Binding input2Binding = binder.bindConsumer(fooTapName, "tap1", module2InputChannel, null); // Another new module is using tap as an input channel String barTapName = "baz.0"; - binder.bindPubSubConsumer(barTapName, module3InputChannel, null, null); + Binding input3Binding = binder.bindConsumer(barTapName, "tap2", 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); + Message inbound = module1InputChannel.receive(5000); assertNotNull(inbound); assertEquals("foo", new String((byte[])inbound.getPayload())); @@ -315,11 +246,11 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { continue; } success = true; - assertEquals("foo", new String((byte[])tapped1.getPayload())); - assertEquals("foo", new String((byte[])tapped2.getPayload())); + assertEquals("foo", new String((byte[]) tapped1.getPayload())); + assertEquals("foo", new String((byte[]) tapped2.getPayload())); } // delete one tap stream is deleted - binder.unbindConsumer(barTapName, module3InputChannel); + binder.unbind(input3Binding); Message message2 = MessageBuilder.withPayload("bar".getBytes()).build(); moduleOutputChannel.send(message2); @@ -327,15 +258,18 @@ public class RawModeKafkaBinderTests extends KafkaBinderTests { Message tapped = module2InputChannel.receive(5000); assertNotNull(tapped); - // Removed tap does not + // removed tap does not assertNull(module3InputChannel.receive(1000)); - // when other tap stream is deleted - binder.unbindConsumer(fooTapName, module2InputChannel); - // Clean up as StreamPlugin would - binder.unbindConsumer("baz.0", moduleInputChannel); - binder.unbindProducers("baz.0"); - binder.unbindConsumers("baz.0"); + // re-subscribed tap does receive the message + input3Binding = binder.bindConsumer(barTapName, "tap2", module3InputChannel, null); + assertNotNull(module3InputChannel.receive(1000)); + + // clean up + binder.unbind(input1Binding); + binder.unbind(input2Binding); + binder.unbind(input3Binding); + binder.unbind(producerBinding); assertTrue(getBindings(binder).isEmpty()); } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitBindingCleaner.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitBindingCleaner.java index ee01466ed..d1ded2561 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitBindingCleaner.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitBindingCleaner.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -25,7 +25,6 @@ import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.cloud.stream.binder.BinderUtils; import org.springframework.cloud.stream.binder.BindingCleaner; import org.springframework.cloud.stream.binder.MessageChannelBinderSupport; import org.springframework.web.client.RestTemplate; @@ -42,7 +41,9 @@ public class RabbitBindingCleaner implements BindingCleaner { private final static Logger logger = LoggerFactory.getLogger(RabbitBindingCleaner.class); - public static final String BINDER_PREFIX = "binder."; + private static final String PREFIX_DELIMITER = "."; + + public static final String BINDER_PREFIX = "binder" + PREFIX_DELIMITER; @Override public Map> clean(String entity, boolean isJob) { @@ -128,7 +129,7 @@ public class RabbitBindingCleaner implements BindingCleaner { return prefix.substring(0, prefix.length() - 1); } else { - return prefix + BinderUtils.GROUP_INDEX_DELIMITER; + return prefix + PREFIX_DELIMITER; } } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java index adb0119b4..864d4c7bf 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-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. @@ -62,7 +62,6 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.cloud.stream.binder.AbstractBindingPropertiesAccessor; import org.springframework.cloud.stream.binder.BinderPropertyKeys; -import org.springframework.cloud.stream.binder.BinderUtils; import org.springframework.cloud.stream.binder.Binding; import org.springframework.cloud.stream.binder.MessageChannelBinderSupport; import org.springframework.cloud.stream.binder.MessageValues; @@ -98,6 +97,7 @@ import com.rabbitmq.client.Envelope; /** * A {@link org.springframework.cloud.stream.binder.Binder} implementation backed by RabbitMQ. + * * @author Mark Fisher * @author Gary Russell * @author Jennifer Hickey @@ -150,40 +150,16 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl .addAll(RABBIT_CONSUMER_PROPERTIES) .build(); - private static final Set SUPPORTED_PUBSUB_CONSUMER_PROPERTIES = new SetBuilder() - .addAll(SUPPORTED_BASIC_CONSUMER_PROPERTIES) - .add(BinderPropertyKeys.DURABLE) - .build(); - /** - * Basic + concurrency. - */ - private static final Set SUPPORTED_NAMED_CONSUMER_PROPERTIES = new SetBuilder() - .addAll(SUPPORTED_BASIC_CONSUMER_PROPERTIES) - .add(BinderPropertyKeys.CONCURRENCY) - .build(); - - /** - * Basic + concurrency + partitioning. + * Basic + durable + concurrency + partitioning. */ private static final Set SUPPORTED_CONSUMER_PROPERTIES = new SetBuilder() .addAll(SUPPORTED_BASIC_CONSUMER_PROPERTIES) + .add(BinderPropertyKeys.DURABLE) .add(BinderPropertyKeys.CONCURRENCY) .add(BinderPropertyKeys.PARTITION_INDEX) .build(); - /** - * Basic + concurrency + reply headers + delivery mode (reply). - */ - private static final Set SUPPORTED_REPLYING_CONSUMER_PROPERTIES = new SetBuilder() - // request - .addAll(SUPPORTED_BASIC_CONSUMER_PROPERTIES) - .add(BinderPropertyKeys.CONCURRENCY) - // reply - .add(RabbitPropertiesAccessor.REPLY_HEADER_PATTERNS) - .add(RabbitPropertiesAccessor.DELIVERY_MODE) - .build(); - /** * Rabbit producer properties. */ @@ -195,39 +171,15 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl .add(BinderPropertyKeys.COMPRESS) .build(); - private static final Set SUPPORTED_PUBSUB_PRODUCER_PROPERTIES = new SetBuilder() - .addAll(SUPPORTED_BASIC_PRODUCER_PROPERTIES) - .addAll(PRODUCER_BATCHING_BASIC_PROPERTIES) - .addAll(PRODUCER_BATCHING_ADVANCED_PROPERTIES) - .build(); - - private static final Set SUPPORTED_NAMED_PRODUCER_PROPERTIES = new SetBuilder() - .addAll(SUPPORTED_BASIC_PRODUCER_PROPERTIES) - .addAll(PRODUCER_BATCHING_BASIC_PROPERTIES) - .addAll(PRODUCER_BATCHING_ADVANCED_PROPERTIES) - .build(); - /** * Partitioning + rabbit producer properties. */ private static final Set SUPPORTED_PRODUCER_PROPERTIES = new SetBuilder() .addAll(PRODUCER_PARTITIONING_PROPERTIES) .addAll(SUPPORTED_BASIC_PRODUCER_PROPERTIES) - .add(BinderPropertyKeys.DIRECT_BINDING_ALLOWED) .addAll(PRODUCER_BATCHING_BASIC_PROPERTIES) .addAll(PRODUCER_BATCHING_ADVANCED_PROPERTIES) - .build(); - - /** - * Basic producer + basic consumer + concurrency + reply headers. - */ - private static final Set SUPPORTED_REQUESTING_PRODUCER_PROPERTIES = new SetBuilder() - // request - .addAll(SUPPORTED_BASIC_PRODUCER_PROPERTIES) - // reply - .addAll(SUPPORTED_BASIC_CONSUMER_PROPERTIES) - .add(BinderPropertyKeys.CONCURRENCY) - .add(RabbitPropertiesAccessor.REPLY_HEADER_PATTERNS) + .add(RabbitPropertiesAccessor.AUTO_BIND_DLQ) .build(); private static final MessagePropertiesConverter inboundMessagePropertiesConverter = @@ -436,51 +388,27 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl } @Override - public void bindConsumer(final String name, MessageChannel moduleInputChannel, Properties properties) { + public Binding doBindConsumer(String name, String group, MessageChannel inputChannel, Properties properties) { + String baseQueueName = groupedName(name, group); if (this.logger.isInfoEnabled()) { - this.logger.info("declaring queue for inbound: " + name); - } - if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX)) { - validateConsumerProperties(name, properties, SUPPORTED_NAMED_CONSUMER_PROPERTIES); - } - else { - validateConsumerProperties(name, properties, SUPPORTED_CONSUMER_PROPERTIES); + this.logger.info("declaring queue for inbound: " + baseQueueName + ", bound to: " + name); } RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); - String queueName = applyPrefix(accessor.getPrefix(this.defaultPrefix), name); - TopicExchange exchange = new TopicExchange(queueName); - declareExchange(queueName, exchange); + validateConsumerProperties(baseQueueName, properties, SUPPORTED_CONSUMER_PROPERTIES); + String prefix = accessor.getPrefix(this.defaultPrefix); + String exchangeName = applyPrefix(prefix, name); + TopicExchange exchange = new TopicExchange(exchangeName); + declareExchange(exchangeName, exchange); + + String queueName = applyPrefix(prefix, baseQueueName); int partitionIndex = accessor.getPartitionIndex(); - String dlqNameRoot = name; if (partitionIndex >= 0) { String partitionSuffix = "-" + partitionIndex; queueName += partitionSuffix; - dlqNameRoot += partitionSuffix; } - Queue queue = new Queue(queueName, true, false, false, queueArgs(accessor, queueName)); - declareQueue(queueName, queue); - autoBindDLQ(dlqNameRoot, accessor); - org.springframework.amqp.core.Binding binding = BindingBuilder.bind(queue).to(exchange).with(queueName); - declareBinding(queueName, binding); - doRegisterConsumer(name, moduleInputChannel, queue, accessor, false); - bindExistingProducerDirectlyIfPossible(name, moduleInputChannel); - } - @Override - public void bindPubSubConsumer(String exchangeName, MessageChannel moduleInputChannel, String group, - Properties properties) { - String name = BinderUtils.groupedName(exchangeName, group); - if (this.logger.isInfoEnabled()) { - this.logger.info("declaring pubsub for inbound: " + name + ", bound to: " + exchangeName); - } - RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); - validateConsumerProperties(name, properties, SUPPORTED_PUBSUB_CONSUMER_PROPERTIES); - String prefix = accessor.getPrefix(this.defaultPrefix); - TopicExchange exchange = new TopicExchange(applyPrefix(prefix, exchangeName)); - declareExchange(exchange.getName(), exchange); Queue queue; boolean durable = accessor.isDurable(this.defaultDurableSubscription); - String queueName = applyPrefix(prefix, name); if (durable) { queue = new Queue(queueName, true, false, false, queueArgs(accessor, queueName)); } @@ -488,11 +416,18 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl queue = new Queue(queueName, false, false, true); } declareQueue(queueName, queue); - declareBinding(queue.getName(), BindingBuilder.bind(queue).to(exchange).with("#")); - doRegisterConsumer(name, moduleInputChannel, queue, accessor, true); - if (durable) { - autoBindDLQ(name, accessor); + if (partitionIndex >= 0) { + String bindingKey = String.format("%s-%d", name, partitionIndex); + declareBinding(queue.getName(), BindingBuilder.bind(queue).to(exchange).with(bindingKey)); } + else { + declareBinding(queue.getName(), BindingBuilder.bind(queue).to(exchange).with("#")); + } + Binding binding = doRegisterConsumer(baseQueueName, group, inputChannel, queue, accessor); + if (durable) { + autoBindDLQ(applyPrefix(prefix, baseQueueName), queueName, accessor); + } + return binding; } private Map queueArgs(RabbitPropertiesAccessor accessor, String queueName) { @@ -504,8 +439,9 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl return args; } - private void doRegisterConsumer(String name, MessageChannel moduleInputChannel, Queue queue, - RabbitPropertiesAccessor properties, boolean isPubSub) { + private Binding doRegisterConsumer(String name, String group, MessageChannel moduleInputChannel, Queue queue, + RabbitPropertiesAccessor properties) { + Binding consumerBinding = null; // Fix for XD-2503 // Temporarily overrides the thread context classloader with the one where the SimpleMessageListenerContainer // is defined @@ -520,15 +456,15 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl listenerContainer.setChannelTransacted(properties.getTransacted(this.defaultChannelTransacted)); listenerContainer.setDefaultRequeueRejected(properties.getRequeueRejected(this .defaultDefaultRequeueRejected)); - if (!isPubSub) { - int concurrency = properties.getConcurrency(this.defaultConcurrency); - concurrency = concurrency > 0 ? concurrency : 1; - listenerContainer.setConcurrentConsumers(concurrency); - int maxConcurrency = properties.getMaxConcurrency(this.defaultMaxConcurrency); - if (maxConcurrency > concurrency) { - listenerContainer.setMaxConcurrentConsumers(maxConcurrency); - } + + int concurrency = properties.getConcurrency(this.defaultConcurrency); + concurrency = concurrency > 0 ? concurrency : 1; + listenerContainer.setConcurrentConsumers(concurrency); + int maxConcurrency = properties.getMaxConcurrency(this.defaultMaxConcurrency); + if (maxConcurrency > concurrency) { + listenerContainer.setMaxConcurrentConsumers(maxConcurrency); } + listenerContainer.setPrefetchCount(properties.getPrefetchCount(this.defaultPrefetchCount)); listenerContainer.setTxSize(properties.getTxSize(this.defaultTxSize)); listenerContainer.setTaskExecutor(new SimpleAsyncTaskExecutor(queue.getName() + "-")); @@ -559,7 +495,7 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl mapper.setReplyHeaderNames(properties.getReplyHeaderPattens(this.defaultReplyHeaderPatterns)); adapter.setHeaderMapper(mapper); adapter.afterPropertiesSet(); - Binding consumerBinding = Binding.forConsumer(name, adapter, moduleInputChannel, properties); + consumerBinding = Binding.forConsumer(name, group, adapter, moduleInputChannel, properties); addBinding(consumerBinding); ReceivingHandler convertingBridge = new ReceivingHandler(); convertingBridge.setOutputChannel(moduleInputChannel); @@ -571,6 +507,7 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl finally { Thread.currentThread().setContextClassLoader(originalClassloader); } + return consumerBinding; } private MessageRecoverer determineRecoverer(String name, RabbitPropertiesAccessor properties) { @@ -587,54 +524,36 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl } } - @Override - public void bindProducer(final String name, MessageChannel moduleOutputChannel, - Properties properties) { - Assert.isInstanceOf(SubscribableChannel.class, moduleOutputChannel); - RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); - if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX)) { - validateProducerProperties(name, properties, SUPPORTED_NAMED_PRODUCER_PROPERTIES); - } - else { - validateProducerProperties(name, properties, SUPPORTED_PRODUCER_PROPERTIES); - } - if (!bindNewProducerDirectlyIfPossible(name, (SubscribableChannel) moduleOutputChannel, accessor)) { - if (this.logger.isInfoEnabled()) { - this.logger.info("declaring queue for outbound: " + name); - } - AmqpOutboundEndpoint queue = this.buildOutboundEndpoint(name, accessor, determineRabbitTemplate(accessor)); - doRegisterProducer(name, moduleOutputChannel, queue, accessor); - } - } - private AmqpOutboundEndpoint buildOutboundEndpoint(final String name, RabbitPropertiesAccessor properties, RabbitTemplate rabbitTemplate) { String prefix = properties.getPrefix(this.defaultPrefix); - String queueName = applyPrefix(prefix, name); + String exchangeName = applyPrefix(prefix, name); String partitionKeyExtractorClass = properties.getPartitionKeyExtractorClass(); Expression partitionKeyExpression = properties.getPartitionKeyExpression(); - TopicExchange exchange = new TopicExchange(queueName); - declareExchange(queueName, exchange); + TopicExchange exchange = new TopicExchange(exchangeName); + declareExchange(exchangeName, exchange); AmqpOutboundEndpoint endpoint = new AmqpOutboundEndpoint(rabbitTemplate); endpoint.setExchangeName(exchange.getName()); + String baseQueueName = exchangeName + ".default"; if (partitionKeyExpression == null && !StringUtils.hasText(partitionKeyExtractorClass)) { - Queue queue = new Queue(queueName, true, false, false, queueArgs(properties, queueName)); - declareQueue(name, queue); - autoBindDLQ(name, properties); - endpoint.setRoutingKey(queueName); - org.springframework.amqp.core.Binding binding = BindingBuilder.bind(queue).to(exchange).with(queueName); - declareBinding(queueName, binding); + Queue queue = new Queue(baseQueueName, true, false, false, queueArgs(properties, baseQueueName)); + declareQueue(baseQueueName, queue); + autoBindDLQ(baseQueueName, baseQueueName, properties); + endpoint.setRoutingKey(name); + org.springframework.amqp.core.Binding binding = BindingBuilder.bind(queue).to(exchange).with(name); + declareBinding(baseQueueName, binding); } else { - endpoint.setExpressionRoutingKey(EXPRESSION_PARSER.parseExpression(buildPartitionRoutingExpression - (queueName))); - // if the stream is partitioned, create one queue for each target partition + endpoint.setExpressionRoutingKey(EXPRESSION_PARSER.parseExpression(buildPartitionRoutingExpression(name))); + // if the stream is partitioned, create one queue for each target partition for the default group for (int i = 0; i < properties.getNextModuleCount(); i++) { String partitionSuffix = "-" + i; - Queue queue = new Queue(queueName + partitionSuffix, true, false, false, queueArgs(properties, queueName)); + String partitionQueueName = baseQueueName + partitionSuffix; + Queue queue = new Queue(partitionQueueName, true, false, false, + queueArgs(properties, partitionQueueName)); declareQueue(queue.getName(), queue); - autoBindDLQ(name + partitionSuffix, properties); - declareBinding(queue.getName(), BindingBuilder.bind(queue).to(exchange).with(queue.getName())); + autoBindDLQ(baseQueueName, baseQueueName + partitionSuffix, properties); + declareBinding(queue.getName(), BindingBuilder.bind(queue).to(exchange).with(name + partitionSuffix)); } } configureOutboundHandler(endpoint, properties); @@ -652,18 +571,14 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl } @Override - public void bindPubSubProducer(String name, MessageChannel moduleOutputChannel, - Properties properties) { - validateProducerProperties(name, properties, SUPPORTED_PUBSUB_PRODUCER_PROPERTIES); + public Binding bindProducer(String name, MessageChannel outputChannel, Properties properties) { + validateProducerProperties(name, properties, SUPPORTED_PRODUCER_PROPERTIES); RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); String exchangeName = applyPrefix(accessor.getPrefix(this.defaultPrefix), name); TopicExchange exchange = new TopicExchange(exchangeName); declareExchange(exchangeName, exchange); - AmqpOutboundEndpoint endpoint = new AmqpOutboundEndpoint(determineRabbitTemplate(accessor)); - endpoint.setExchangeName(exchangeName); - endpoint.setRoutingKey(name); - configureOutboundHandler(endpoint, accessor); - doRegisterProducer(name, moduleOutputChannel, endpoint, accessor); + AmqpOutboundEndpoint endpoint = this.buildOutboundEndpoint(name, accessor, determineRabbitTemplate(accessor)); + return doRegisterProducer(name, outputChannel, endpoint, accessor); } private RabbitTemplate determineRabbitTemplate(RabbitPropertiesAccessor properties) { @@ -691,12 +606,12 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl return rabbitTemplate; } - private void doRegisterProducer(final String name, MessageChannel moduleOutputChannel, + private Binding doRegisterProducer(final String name, MessageChannel moduleOutputChannel, AmqpOutboundEndpoint delegate, RabbitPropertiesAccessor properties) { - this.doRegisterProducer(name, moduleOutputChannel, delegate, null, properties); + return this.doRegisterProducer(name, moduleOutputChannel, delegate, null, properties); } - private void doRegisterProducer(final String name, MessageChannel moduleOutputChannel, + private Binding doRegisterProducer(final String name, MessageChannel moduleOutputChannel, AmqpOutboundEndpoint delegate, String replyTo, RabbitPropertiesAccessor properties) { Assert.isInstanceOf(SubscribableChannel.class, moduleOutputChannel); MessageHandler handler = new SendingHandler(delegate, replyTo, properties); @@ -704,72 +619,33 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl consumer.setBeanFactory(getBeanFactory()); consumer.setBeanName("outbound." + name); consumer.afterPropertiesSet(); - Binding producerBinding = Binding.forProducer(name, moduleOutputChannel, consumer, properties); + Binding producerBinding = Binding.forProducer(name, moduleOutputChannel, consumer, properties); addBinding(producerBinding); producerBinding.start(); - } - - @Override - public void bindRequestor(String name, MessageChannel requests, MessageChannel replies, - Properties properties) { - if (this.logger.isInfoEnabled()) { - this.logger.info("binding requestor: " + name); - } - validateProducerProperties(name, properties, SUPPORTED_REQUESTING_PRODUCER_PROPERTIES); - Assert.isInstanceOf(SubscribableChannel.class, requests); - RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); - String queueName = applyRequests(name); - AmqpOutboundEndpoint queue = this.buildOutboundEndpoint(queueName, accessor, this.rabbitTemplate); - queue.setBeanFactory(this.getBeanFactory()); - - String replyQueueName = accessor.getPrefix(this.defaultPrefix) + name + ".replies." - + this.getIdGenerator().generateId(); - this.doRegisterProducer(name, requests, queue, replyQueueName, accessor); - Queue replyQueue = new Queue(replyQueueName, false, false, true); // auto-delete - declareQueue(replyQueueName, replyQueue); - this.doRegisterConsumer(name, replies, replyQueue, accessor, false); - } - - @Override - public void bindReplier(String name, MessageChannel requests, MessageChannel replies, - Properties properties) { - if (this.logger.isInfoEnabled()) { - this.logger.info("binding replier: " + name); - } - validateConsumerProperties(name, properties, SUPPORTED_REPLYING_CONSUMER_PROPERTIES); - RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); - Queue requestQueue = new Queue(applyPrefix(accessor.getPrefix(this.defaultPrefix), applyRequests(name))); - declareQueue(requestQueue.getName(), requestQueue); - this.doRegisterConsumer(name, requests, requestQueue, accessor, false); - - AmqpOutboundEndpoint replyQueue = new AmqpOutboundEndpoint(this.rabbitTemplate); - replyQueue.setExpressionRoutingKey(EXPRESSION_PARSER.parseExpression("headers['" + AmqpHeaders.REPLY_TO + - "']")); - configureOutboundHandler(replyQueue, accessor); - doRegisterProducer(name, replies, replyQueue, accessor); + return producerBinding; } /** * If so requested, declare the DLX/DLQ and bind it. The DLQ is bound to the DLX with a routing key of the original * queue name because we use default exchange routing by queue name for the original message. - * @param name The name. + * @param queueName The base name for the queue (including the binder prefix, if any). + * @param routingKey The routing key for the queue. * @param properties The properties accessor. */ - private void autoBindDLQ(final String name, RabbitPropertiesAccessor properties) { + private void autoBindDLQ(final String queueName, String routingKey, RabbitPropertiesAccessor properties) { if (this.logger.isDebugEnabled()) { this.logger.debug("autoBindDLQ=" + properties.getAutoBindDLQ(this.defaultAutoBindDLQ) - + " for: " + name); + + " for: " + queueName); } if (properties.getAutoBindDLQ(this.defaultAutoBindDLQ)) { String prefix = properties.getPrefix(this.defaultPrefix); - String queueName = applyPrefix(prefix, name); String dlqName = constructDLQName(queueName); Queue dlq = new Queue(dlqName); declareQueue(dlqName, dlq); final String dlxName = deadLetterExchangeName(prefix); final DirectExchange dlx = new DirectExchange(dlxName); declareExchange(dlxName, dlx); - declareBinding(dlqName, BindingBuilder.bind(dlq).to(dlx).with(queueName)); + declareBinding(dlqName, BindingBuilder.bind(dlq).to(dlx).with(routingKey)); } } @@ -814,21 +690,10 @@ public class RabbitMessageChannelBinder extends MessageChannelBinderSupport impl } @Override - public void unbindConsumer(String name, MessageChannel channel) { - super.unbindConsumer(name, channel); - cleanAutoDeclareContext(name); - } - - @Override - public void unbindConsumers(String name) { - super.unbindConsumers(name); - cleanAutoDeclareContext(name); - } - - @Override - public void unbindPubSubConsumers(String name, String group) { - super.unbindPubSubConsumers(name, group); - cleanAutoDeclareContext(BinderUtils.groupedName(name, group)); + protected void afterUnbind(Binding binding) { + if (Binding.Type.consumer.equals(binding.getType())) { + cleanAutoDeclareContext(groupedName(binding.getName(), binding.getGroup())); + } } private void addToAutoDeclareContext(String name, Object bean) { diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryIntegrationTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryIntegrationTests.java index 29f00cf48..e13160799 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryIntegrationTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -46,7 +46,7 @@ public class LocalizedQueueConnectionFactoryIntegrationTests { @Before public void setup() { - ConnectionFactory defaultConnectionFactory = new CachingConnectionFactory("localhost"); + ConnectionFactory defaultConnectionFactory = rabbitAvailableRule.getResource(); String[] addresses = new String[] { "localhost:9999", "localhost:5672" }; String[] adminAddresses = new String[] { "http://localhost:15672", "http://localhost:15672" }; String[] nodes = new String[] { "foo@bar", "rabbit@localhost" }; @@ -66,6 +66,8 @@ public class LocalizedQueueConnectionFactoryIntegrationTests { RabbitTemplate template = new RabbitTemplate(targetConnectionFactory); template.convertAndSend("", queue.getName(), "foo"); assertEquals("foo", template.receiveAndConvert(queue.getName())); + ((CachingConnectionFactory) targetConnectionFactory).destroy(); + admin.deleteQueue(queue.getName()); } } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderCleanerTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderCleanerTests.java index a772e7774..68058fca9 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderCleanerTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderCleanerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -69,8 +69,8 @@ public class RabbitBinderCleanerTests { CachingConnectionFactory connectionFactory = rabbitWithMgmtEnabled.getResource(); RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory); for (int i = 0; i < 5; i++) { - String queue1Name = MessageChannelBinderSupport.applyPrefix(BINDER_PREFIX, stream1 + "." + i); - String queue2Name = MessageChannelBinderSupport.applyPrefix(BINDER_PREFIX, stream2 + "." + i); + String queue1Name = MessageChannelBinderSupport.applyPrefix(BINDER_PREFIX, stream1 + ".default." + i); + String queue2Name = MessageChannelBinderSupport.applyPrefix(BINDER_PREFIX, stream2 + ".default." + i); if (firstQueue == null) { firstQueue = queue1Name; } @@ -110,7 +110,7 @@ public class RabbitBinderCleanerTests { @Override public Void doInRabbit(Channel channel) throws Exception { - String queueName = MessageChannelBinderSupport.applyPrefix(BINDER_PREFIX, stream1 + "." + 4); + String queueName = MessageChannelBinderSupport.applyPrefix(BINDER_PREFIX, stream1 + ".default." + 4); String consumerTag = channel.basicConsume(queueName, new DefaultConsumer(channel)); try { waitForConsumerStateNot(queueName, 0); @@ -160,8 +160,8 @@ public class RabbitBinderCleanerTests { // should *not* clean stream2 assertEquals(10, cleanedQueues.size()); for (int i = 0; i < 5; i++) { - assertEquals(BINDER_PREFIX + stream1 + "." + i, cleanedQueues.get(i * 2)); - assertEquals(BINDER_PREFIX + stream1 + "." + i + ".dlq", cleanedQueues.get(i * 2 + 1)); + assertEquals(BINDER_PREFIX + stream1 + ".default." + i, cleanedQueues.get(i * 2)); + assertEquals(BINDER_PREFIX + stream1 + ".default." + i + ".dlq", cleanedQueues.get(i * 2 + 1)); } List cleanedExchanges = cleanedMap.get("exchanges"); assertEquals(6, cleanedExchanges.size()); @@ -172,7 +172,7 @@ public class RabbitBinderCleanerTests { cleanedQueues = cleanedMap.get("queues"); assertEquals(5, cleanedQueues.size()); for (int i = 0; i < 5; i++) { - assertEquals(BINDER_PREFIX + stream2 + "." + i, cleanedQueues.get(i)); + assertEquals(BINDER_PREFIX + stream2 + ".default." + i, cleanedQueues.get(i)); } cleanedExchanges = cleanedMap.get("exchanges"); assertEquals(6, cleanedExchanges.size()); diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java index 757fbcdb1..7ecff915b 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-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. @@ -16,7 +16,6 @@ package org.springframework.cloud.stream.binder.rabbit; -import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.startsWith; @@ -26,8 +25,6 @@ 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 static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -58,14 +55,13 @@ import org.springframework.amqp.support.AmqpHeaders; import org.springframework.amqp.support.postprocessor.DelegatingDecompressingPostProcessor; import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.cloud.stream.binder.AbstractTestBinder; import org.springframework.cloud.stream.binder.Binder; import org.springframework.cloud.stream.binder.BinderPropertyKeys; import org.springframework.cloud.stream.binder.Binding; import org.springframework.cloud.stream.binder.PartitionCapableBinderTests; import org.springframework.cloud.stream.binder.Spy; import org.springframework.cloud.stream.test.junit.rabbit.RabbitTestSupport; -import org.springframework.context.support.AbstractApplicationContext; import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.channel.QueueChannel; @@ -110,8 +106,8 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { Binder binder = getBinder(); DirectChannel moduleOutputChannel = new DirectChannel(); DirectChannel moduleInputChannel = new DirectChannel(); - binder.bindProducer("bad.0", moduleOutputChannel, null); - binder.bindConsumer("bad.0", moduleInputChannel, null); + Binding producerBinding = binder.bindProducer("bad.0", moduleOutputChannel, null); + Binding consumerBinding = binder.bindConsumer("bad.0", "test", moduleInputChannel, null); Message message = MessageBuilder.withPayload("bad").setHeader(MessageHeaders.CONTENT_TYPE, "foo/bar").build(); final CountDownLatch latch = new CountDownLatch(3); @@ -125,8 +121,8 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { }); moduleOutputChannel.send(message); assertTrue(latch.await(10, TimeUnit.SECONDS)); - binder.unbindConsumers("bad.0"); - binder.unbindProducers("bad.0"); + binder.unbind(consumerBinding); + binder.unbind(producerBinding); } @Test @@ -134,15 +130,15 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { Binder binder = getBinder(); Properties properties = new Properties(); properties.put("transacted", "true"); // test transacted with defaults; not allowed with ackmode NONE - binder.bindConsumer("props.0", new DirectChannel(), properties); + Binding consumerBinding = binder.bindConsumer("props.0", null, new DirectChannel(), properties); @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + List> bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); assertEquals(1, bindings.size()); AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); SimpleMessageListenerContainer container = TestUtils.getPropertyValue(endpoint, "messageListenerContainer", SimpleMessageListenerContainer.class); assertEquals(AcknowledgeMode.AUTO, container.getAcknowledgeMode()); - assertEquals(RabbitMessageChannelBinder.DEFAULT_RABBIT_PREFIX + "props.0", container.getQueueNames()[0]); + assertEquals(RabbitMessageChannelBinder.DEFAULT_RABBIT_PREFIX + "props.0.default", container.getQueueNames()[0]); assertTrue(TestUtils.getPropertyValue(container, "transactional", Boolean.class)); assertEquals(1, TestUtils.getPropertyValue(container, "concurrentConsumers")); assertNull(TestUtils.getPropertyValue(container, "maxConcurrentConsumers")); @@ -154,7 +150,7 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { assertEquals(1000L, TestUtils.getPropertyValue(retry, "retryOperations.backOffPolicy.initialInterval")); assertEquals(10000L, TestUtils.getPropertyValue(retry, "retryOperations.backOffPolicy.maxInterval")); assertEquals(2.0, TestUtils.getPropertyValue(retry, "retryOperations.backOffPolicy.multiplier")); - binder.unbindConsumers("props.0"); + binder.unbind(consumerBinding); assertEquals(0, bindings.size()); properties = new Properties(); @@ -171,46 +167,26 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { properties.put("requeue", "false"); properties.put("txSize", "10"); properties.put("partitionIndex", 0); - binder.bindConsumer("props.0", new DirectChannel(), properties); + consumerBinding = binder.bindConsumer("props.0", "test", new DirectChannel(), properties); @SuppressWarnings("unchecked") - List bindingsNow = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + List> bindingsNow = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); assertEquals(1, bindingsNow.size()); endpoint = bindingsNow.get(0).getEndpoint(); container = verifyContainer(endpoint); - assertEquals("foo.props.0", container.getQueueNames()[0]); + assertEquals("foo.props.0.test", container.getQueueNames()[0]); - try { - binder.bindPubSubConsumer("dummy", null, null, properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support consumer properties: "), - containsString("partitionIndex"), - containsString("concurrency"), - containsString(" for dummy."))); - } - try { - binder.bindConsumer("queue:dummy", null, properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertEquals(getClassUnderTestName() + " does not support consumer property: partitionIndex for queue:dummy.", - e.getMessage()); - } - - binder.unbindConsumers("props.0"); + binder.unbind(consumerBinding); assertEquals(0, bindingsNow.size()); } @Test public void testProducerProperties() throws Exception { Binder binder = getBinder(); - binder.bindProducer("props.0", new DirectChannel(), null); + Binding producerBinding = binder.bindProducer("props.0", new DirectChannel(), null); @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + List> bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); assertEquals(1, bindings.size()); AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); MessageDeliveryMode mode = TestUtils.getPropertyValue(endpoint, "handler.delegate.defaultDeliveryMode", @@ -219,7 +195,7 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { List requestHeaders = TestUtils.getPropertyValue(endpoint, "handler.delegate.headerMapper.requestHeaderMatcher.strategies", List.class); assertEquals(2, requestHeaders.size()); - binder.unbindProducers("props.0"); + binder.unbind(producerBinding); assertEquals(0, bindings.size()); Properties properties = new Properties(); @@ -232,11 +208,11 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { properties.put("partitionSelectorClass", "foo"); properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "1"); - binder.bindProducer("props.0", new DirectChannel(), properties); + producerBinding = binder.bindProducer("props.0", new DirectChannel(), properties); assertEquals(1, bindings.size()); endpoint = bindings.get(0).getEndpoint(); assertEquals( - "'foo.props.0-' + headers['partition']", + "'props.0-' + headers['partition']", TestUtils.getPropertyValue(endpoint, "handler.delegate.routingKeyExpression", SpelExpression.class).getExpressionString()); mode = TestUtils.getPropertyValue(endpoint, "handler.delegate.defaultDeliveryMode", @@ -244,167 +220,7 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { assertEquals(MessageDeliveryMode.NON_PERSISTENT, mode); verifyFooRequestProducer(endpoint); - try { - binder.bindPubSubProducer("dummy", new DirectChannel(), properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support producer properties: "), - containsString("partitionSelectorExpression"), - containsString("partitionKeyExtractorClass"), - containsString("partitionKeyExpression"), - containsString("partitionSelectorClass"))); - assertThat(e.getMessage(), containsString("for dummy.")); - } - try { - binder.bindProducer("queue:dummy", new DirectChannel(), properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support producer properties: "), - containsString("partitionSelectorExpression"), - containsString("partitionKeyExtractorClass"), - containsString("partitionKeyExpression"), - containsString("partitionSelectorClass"))); - assertThat(e.getMessage(), containsString("for queue:dummy.")); - } - - binder.unbindProducers("props.0"); - assertEquals(0, bindings.size()); - } - - @Test - public void testRequestReplyRequestorProperties() throws Exception { - Binder binder = getBinder(); - Properties properties = new Properties(); - properties.put("prefix", "foo."); - properties.put("deliveryMode", "NON_PERSISTENT"); - - properties.put("requestHeaderPatterns", "foo"); - properties.put("replyHeaderPatterns", "bar"); - - properties.put("ackMode", "NONE"); - properties.put("backOffInitialInterval", "2000"); - properties.put("backOffMaxInterval", "20000"); - properties.put("backOffMultiplier", "5.0"); - properties.put("concurrency", "2"); - properties.put("maxAttempts", "23"); - properties.put("maxConcurrency", "3"); - properties.put("prefix", "foo."); - properties.put("prefetch", "20"); - properties.put("requeue", "false"); - properties.put("txSize", "10"); - - binder.bindRequestor("props.0", new DirectChannel(), new DirectChannel(), properties); - @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); - - assertEquals(2, bindings.size()); - AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); // producer - MessageDeliveryMode mode = TestUtils.getPropertyValue(endpoint, "handler.delegate.defaultDeliveryMode", - MessageDeliveryMode.class); - assertEquals(MessageDeliveryMode.NON_PERSISTENT, mode); - verifyFooRequestBarReplyProducer(endpoint); - - endpoint = bindings.get(1).getEndpoint(); // consumer - - verifyContainer(endpoint); - - verifyBarReplyConsumer(endpoint); - - properties.put("partitionKeyExpression", "'foo'"); - properties.put("partitionKeyExtractorClass", "foo"); - properties.put("partitionSelectorExpression", "0"); - properties.put("partitionSelectorClass", "foo"); - properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "1"); - properties.put("partitionIndex", "0"); - try { - binder.bindRequestor("dummy", null, null, properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support producer properties: "), - containsString("partitionSelectorExpression"), - containsString("partitionKeyExtractorClass"), - containsString("partitionKeyExpression"), - containsString("partitionSelectorClass"))); - assertThat(e.getMessage(), allOf(containsString("partitionIndex"), containsString("for dummy."))); - } - - binder.unbindConsumers("props.0"); - binder.unbindProducers("props.0"); - assertEquals(0, bindings.size()); - } - - @Test - public void testRequestReplyReplierProperties() throws Exception { - Binder binder = getBinder(); - Properties properties = new Properties(); - properties.put("prefix", "foo."); - properties.put("deliveryMode", "NON_PERSISTENT"); - - properties.put("requestHeaderPatterns", "foo"); - properties.put("replyHeaderPatterns", "bar"); - - properties.put("ackMode", "NONE"); - properties.put("backOffInitialInterval", "2000"); - properties.put("backOffMaxInterval", "20000"); - properties.put("backOffMultiplier", "5.0"); - properties.put("concurrency", "2"); - properties.put("maxAttempts", "23"); - properties.put("maxConcurrency", "3"); - properties.put("prefix", "foo."); - properties.put("prefetch", "20"); - properties.put("requeue", "false"); - properties.put("txSize", "10"); - - binder.bindReplier("props.0", new DirectChannel(), new DirectChannel(), properties); - @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); - - assertEquals(2, bindings.size()); - AbstractEndpoint endpoint = bindings.get(1).getEndpoint(); // producer - assertEquals( - "headers['amqp_replyTo']", - TestUtils.getPropertyValue(endpoint, "handler.delegate.routingKeyExpression", - SpelExpression.class).getExpressionString()); - MessageDeliveryMode mode = TestUtils.getPropertyValue(endpoint, "handler.delegate.defaultDeliveryMode", - MessageDeliveryMode.class); - assertEquals(MessageDeliveryMode.NON_PERSISTENT, mode); - - verifyFooRequestBarReplyProducer(endpoint); - - endpoint = bindings.get(0).getEndpoint(); // consumer - - verifyContainer(endpoint); - - verifyBarReplyConsumer(endpoint); - - properties.put("partitionKeyExpression", "'foo'"); - properties.put("partitionKeyExtractorClass", "foo"); - properties.put("partitionSelectorExpression", "0"); - properties.put("partitionSelectorClass", "foo"); - properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "1"); - properties.put("partitionIndex", "0"); - try { - binder.bindReplier("dummy", null, null, properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support consumer properties: "), - containsString("partitionSelectorExpression"), - containsString("partitionKeyExtractorClass"), - containsString("partitionKeyExpression"), - containsString("partitionSelectorClass"))); - assertThat(e.getMessage(), allOf(containsString("partitionIndex"), containsString("for dummy."))); - } - - binder.unbindConsumers("props.0"); - binder.unbindProducers("props.0"); + binder.unbind(producerBinding); assertEquals(0, bindings.size()); } @@ -430,14 +246,14 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { } }); - binder.bindPubSubConsumer("durabletest.0", moduleInputChannel, "tgroup", properties); + Binding consumerBinding = binder.bindConsumer("durabletest.0", "tgroup", moduleInputChannel, properties); RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource()); template.convertAndSend(TEST_PREFIX + "durabletest.0", "", "foo"); int n = 0; while (n++ < 100) { - Object deadLetter = template.receiveAndConvert(TEST_PREFIX + "tgroup.durabletest.0.dlq"); + Object deadLetter = template.receiveAndConvert(TEST_PREFIX + "durabletest.0.tgroup.dlq"); if (deadLetter != null) { assertEquals("foo", deadLetter); break; @@ -446,13 +262,8 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { } assertTrue(n < 100); - binder.unbindConsumer("durabletest.0", moduleInputChannel); - binder.unbindPubSubConsumers("durabletest.0", "tgroup"); - assertNotNull(admin.getQueueProperties(TEST_PREFIX + "tgroup.durabletest.0.dlq")); - admin.deleteQueue(TEST_PREFIX + "tgroup.durabletest.0.dlq"); - admin.deleteQueue(TEST_PREFIX + "tgroup.durabletest.0"); - admin.deleteExchange(TEST_PREFIX + "durabletest.0"); - admin.deleteExchange(TEST_PREFIX + "DLX"); + binder.unbind(consumerBinding); + assertNotNull(admin.getQueueProperties(TEST_PREFIX + "durabletest.0.tgroup.dlq")); } @Test @@ -476,18 +287,14 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { } }); - binder.bindPubSubConsumer("nondurabletest.0", moduleInputChannel, "tgroup", properties); + Binding consumerBinding = binder.bindConsumer("nondurabletest.0", "tgroup", moduleInputChannel, properties); - binder.unbindPubSubConsumers("nondurabletest.0", "tgroup"); + binder.unbind(consumerBinding); assertNull(admin.getQueueProperties(TEST_PREFIX + "nondurabletest.0.dlq")); - admin.deleteQueue(TEST_PREFIX + "tgroup.nondurabletest.0"); - admin.deleteExchange(TEST_PREFIX + "nondurabletest.0"); } @Test public void testAutoBindDLQ() throws Exception { - RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource()); - Binder binder = getBinder(); Properties properties = new Properties(); properties.put("prefix", TEST_PREFIX); @@ -504,14 +311,14 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { } }); - binder.bindConsumer("dlqtest", moduleInputChannel, properties); + Binding consumerBinding = binder.bindConsumer("dlqtest", null, moduleInputChannel, properties); RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource()); - template.convertAndSend("", TEST_PREFIX + "dlqtest", "foo"); + template.convertAndSend("", TEST_PREFIX + "dlqtest.default", "foo"); int n = 0; while (n++ < 100) { - Object deadLetter = template.receiveAndConvert(TEST_PREFIX + "dlqtest.dlq"); + Object deadLetter = template.receiveAndConvert(TEST_PREFIX + "dlqtest.default.dlq"); if (deadLetter != null) { assertEquals("foo", deadLetter); break; @@ -520,10 +327,176 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { } assertTrue(n < 100); - binder.unbindConsumer("dlqtest", moduleInputChannel); - admin.deleteQueue(TEST_PREFIX + "dlqtest.dlq"); - admin.deleteQueue(TEST_PREFIX + "dlqtest"); - admin.deleteExchange(TEST_PREFIX + "DLX"); + binder.unbind(consumerBinding); + } + + @Test + public void testAutoBindDLQPartionedConsumerFirst() throws Exception { + Binder binder = getBinder(); + Properties properties = new Properties(); + properties.put("prefix", "bindertest."); + properties.put("autoBindDLQ", "true"); + properties.put("maxAttempts", "1"); // disable retry + properties.put("requeue", "false"); + properties.put("partitionIndex", "0"); + DirectChannel input0 = new DirectChannel(); + input0.setBeanName("test.input0DLQ"); + Binding input0Binding = binder.bindConsumer("partDLQ.0", "dlqPartGrp", input0, properties); + Binding defaultConsumerBinding1 = binder.bindConsumer("partDLQ.0", null, new QueueChannel(), properties); + properties.put("partitionIndex", "1"); + DirectChannel input1 = new DirectChannel(); + input1.setBeanName("test.input1DLQ"); + Binding input1Binding = binder.bindConsumer("partDLQ.0", "dlqPartGrp", input1, properties); + Binding defaultConsumerBinding2 = binder.bindConsumer("partDLQ.0", null, new QueueChannel(), properties); + + properties.clear(); + properties.put("prefix", "bindertest."); + properties.put("autoBindDLQ", "true"); + properties.put("partitionKeyExtractorClass", "org.springframework.cloud.stream.binder.PartitionTestSupport"); + properties.put("partitionSelectorClass", "org.springframework.cloud.stream.binder.PartitionTestSupport"); + properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "2"); + DirectChannel output = new DirectChannel(); + output.setBeanName("test.output"); + Binding outputBinding = binder.bindProducer("partDLQ.0", output, properties); + + final CountDownLatch latch0 = new CountDownLatch(1); + input0.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + if (latch0.getCount() <= 0) { + throw new RuntimeException("dlq"); + } + latch0.countDown(); + } + + }); + + final CountDownLatch latch1 = new CountDownLatch(1); + input1.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + if (latch1.getCount() <= 0) { + throw new RuntimeException("dlq"); + } + latch1.countDown(); + } + + }); + + output.send(new GenericMessage(1)); + assertTrue(latch1.await(10, TimeUnit.SECONDS)); + + output.send(new GenericMessage(0)); + assertTrue(latch0.await(10, TimeUnit.SECONDS)); + + output.send(new GenericMessage(1)); + + RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource()); + template.setReceiveTimeout(10000); + + String streamDLQName = "bindertest.partDLQ.0.dlqPartGrp.dlq"; + + org.springframework.amqp.core.Message received = template.receive(streamDLQName); + assertNotNull(received); + assertEquals(1, received.getMessageProperties().getHeaders().get("partition")); + + output.send(new GenericMessage(0)); + received = template.receive(streamDLQName); + assertNotNull(received); + assertEquals(0, received.getMessageProperties().getHeaders().get("partition")); + + binder.unbind(input0Binding); + binder.unbind(input1Binding); + binder.unbind(defaultConsumerBinding1); + binder.unbind(defaultConsumerBinding2); + binder.unbind(outputBinding); + } + + @Test + public void testAutoBindDLQPartionedProducerFirst() throws Exception { + Binder binder = getBinder(); + Properties properties = new Properties(); + + properties.put("prefix", "bindertest."); + properties.put("autoBindDLQ", "true"); + properties.put("partitionKeyExtractorClass", "org.springframework.cloud.stream.binder.PartitionTestSupport"); + properties.put("partitionSelectorClass", "org.springframework.cloud.stream.binder.PartitionTestSupport"); + properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "2"); + DirectChannel output = new DirectChannel(); + output.setBeanName("test.output"); + Binding outputBinding = binder.bindProducer("partDLQ.1", output, properties); + + properties.clear(); + properties.put("prefix", "bindertest."); + properties.put("autoBindDLQ", "true"); + properties.put("maxAttempts", "1"); // disable retry + properties.put("requeue", "false"); + properties.put("partitionIndex", "0"); + DirectChannel input0 = new DirectChannel(); + input0.setBeanName("test.input0DLQ"); + Binding input0Binding = binder.bindConsumer("partDLQ.1", "dlqPartGrp", input0, properties); + Binding defaultConsumerBinding1 = binder.bindConsumer("partDLQ.1", null, new QueueChannel(), properties); + properties.put("partitionIndex", "1"); + DirectChannel input1 = new DirectChannel(); + input1.setBeanName("test.input1DLQ"); + Binding input1Binding = binder.bindConsumer("partDLQ.1", "dlqPartGrp", input1, properties); + Binding defaultConsumerBinding2 = binder.bindConsumer("partDLQ.1", null, new QueueChannel(), properties); + + final CountDownLatch latch0 = new CountDownLatch(1); + input0.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + if (latch0.getCount() <= 0) { + throw new RuntimeException("dlq"); + } + latch0.countDown(); + } + + }); + + final CountDownLatch latch1 = new CountDownLatch(1); + input1.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + if (latch1.getCount() <= 0) { + throw new RuntimeException("dlq"); + } + latch1.countDown(); + } + + }); + + output.send(new GenericMessage(1)); + assertTrue(latch1.await(10, TimeUnit.SECONDS)); + + output.send(new GenericMessage(0)); + assertTrue(latch0.await(10, TimeUnit.SECONDS)); + + output.send(new GenericMessage(1)); + + RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource()); + template.setReceiveTimeout(10000); + + String streamDLQName = "bindertest.partDLQ.1.dlqPartGrp.dlq"; + + org.springframework.amqp.core.Message received = template.receive(streamDLQName); + assertNotNull(received); + assertEquals(1, received.getMessageProperties().getHeaders().get("partition")); + + output.send(new GenericMessage(0)); + received = template.receive(streamDLQName); + assertNotNull(received); + assertEquals(0, received.getMessageProperties().getHeaders().get("partition")); + + binder.unbind(input0Binding); + binder.unbind(input1Binding); + binder.unbind(defaultConsumerBinding1); + binder.unbind(defaultConsumerBinding2); + binder.unbind(outputBinding); } @Test @@ -532,8 +505,8 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource()); Map args = new HashMap(); args.put("x-dead-letter-exchange", TEST_PREFIX + "DLX"); - args.put("x-dead-letter-routing-key", TEST_PREFIX + "dlqpubtest"); - Queue queue = new Queue(TEST_PREFIX + "dlqpubtest", true, false, false, args); + args.put("x-dead-letter-routing-key", TEST_PREFIX + "dlqpubtest.default"); + Queue queue = new Queue(TEST_PREFIX + "dlqpubtest.default", true, false, false, args); admin.declareQueue(queue); Binder binder = getBinder(); @@ -553,14 +526,14 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { } }); - binder.bindConsumer("dlqpubtest", moduleInputChannel, properties); + Binding consumerBinding = binder.bindConsumer("dlqpubtest", "default", moduleInputChannel, properties); RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource()); - template.convertAndSend("", TEST_PREFIX + "dlqpubtest", "foo"); + template.convertAndSend("", TEST_PREFIX + "dlqpubtest.default", "foo"); int n = 0; while (n++ < 100) { - org.springframework.amqp.core.Message deadLetter = template.receive(TEST_PREFIX + "dlqpubtest.dlq"); + org.springframework.amqp.core.Message deadLetter = template.receive(TEST_PREFIX + "dlqpubtest.default.dlq"); if (deadLetter != null) { assertEquals("foo", new String(deadLetter.getBody())); assertNotNull(deadLetter.getMessageProperties().getHeaders().get("x-exception-stacktrace")); @@ -570,10 +543,7 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { } assertTrue(n < 100); - binder.unbindConsumer("dlqpubtest", moduleInputChannel); - admin.deleteQueue(TEST_PREFIX + "dlqpubtest.dlq"); - admin.deleteQueue(TEST_PREFIX + "dlqpubtest"); - admin.deleteExchange(TEST_PREFIX + "DLX"); + binder.unbind(consumerBinding); } @SuppressWarnings("unchecked") @@ -591,9 +561,9 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { DirectChannel output = new DirectChannel(); output.setBeanName("batchingProducer"); - binder.bindProducer("batching.0", output, properties); + Binding producerBinding = binder.bindProducer("batching.0", output, properties); - while (template.receive(RabbitMessageChannelBinder.DEFAULT_RABBIT_PREFIX + "batching.0") != null) { + while (template.receive(RabbitMessageChannelBinder.DEFAULT_RABBIT_PREFIX + "batching.0.default") != null) { } Log logger = spy(TestUtils.getPropertyValue(binder, "binder.compressingPostProcessor.logger", Log.class)); @@ -606,7 +576,7 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { output.send(new GenericMessage<>("foo".getBytes())); output.send(new GenericMessage<>("bar".getBytes())); - Object out = spyOn("batching.0").receive(false); + Object out = spyOn("batching.0.default").receive(false); assertThat(out, instanceOf(byte[].class)); assertEquals("\u0000\u0000\u0000\u0003foo\u0000\u0000\u0000\u0003bar", new String((byte[]) out)); @@ -616,7 +586,7 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { QueueChannel input = new QueueChannel(); input.setBeanName("batchingConsumer"); - binder.bindConsumer("batching.0", input, null); + Binding consumerBinding = binder.bindConsumer("batching.0", "test", input, null); output.send(new GenericMessage<>("foo".getBytes())); output.send(new GenericMessage<>("bar".getBytes())); @@ -629,8 +599,8 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { assertEquals("bar", new String(in.getPayload())); assertNull(in.getHeaders().get(AmqpHeaders.DELIVERY_MODE)); - binder.unbindProducers("batching.0"); - binder.unbindConsumers("batching.0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } /* @@ -641,53 +611,51 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { public void testLateBinding() throws Exception { RabbitTestSupport.RabbitProxy proxy = new RabbitTestSupport.RabbitProxy(); CachingConnectionFactory cf = new CachingConnectionFactory("localhost", proxy.getPort()); - RabbitMessageChannelBinder binder = new RabbitMessageChannelBinder(cf); - AbstractApplicationContext applicationContext = mock(AbstractApplicationContext.class); - when(applicationContext.getBeanFactory()).thenReturn(mock(ConfigurableListableBeanFactory.class)); - binder.setApplicationContext(applicationContext); - binder.setDefaultAutoBindDLQ(true); - binder.afterPropertiesSet(); + RabbitMessageChannelBinder rabbitBinder = new RabbitMessageChannelBinder(cf); + rabbitBinder.setDefaultAutoBindDLQ(true); + AbstractTestBinder binder = new RabbitTestBinder(cf, rabbitBinder); + Properties properties = new Properties(); properties.put("prefix", "latebinder."); MessageChannel moduleOutputChannel = new DirectChannel(); - binder.bindProducer("late.0", moduleOutputChannel, properties); + Binding late0ProducerBinding = binder.bindProducer("late.0", moduleOutputChannel, properties); QueueChannel moduleInputChannel = new QueueChannel(); - binder.bindConsumer("late.0", moduleInputChannel, properties); + Binding late0ConsumerBinding = binder.bindConsumer("late.0", "test", moduleInputChannel, properties); properties.put("partitionKeyExpression", "payload.equals('0') ? 0 : 1"); properties.put("partitionSelectorExpression", "hashCode()"); properties.put("nextModuleCount", "2"); MessageChannel partOutputChannel = new DirectChannel(); - binder.bindProducer("partlate.0", partOutputChannel, properties); + Binding partlate0ProducerBinding = binder.bindProducer("partlate.0", partOutputChannel, properties); QueueChannel partInputChannel0 = new QueueChannel(); QueueChannel partInputChannel1 = new QueueChannel(); properties.clear(); properties.put("prefix", "latebinder."); properties.put("partitionIndex", "0"); - binder.bindConsumer("partlate.0", partInputChannel0, properties); + Binding partlate0Consumer0Binding = binder.bindConsumer("partlate.0", "test", partInputChannel0, properties); properties.put("partitionIndex", "1"); - binder.bindConsumer("partlate.0", partInputChannel1, properties); + Binding partlate0Consumer1Binding = binder.bindConsumer("partlate.0", "test", partInputChannel1, properties); - binder.setDefaultAutoBindDLQ(false); + rabbitBinder.setDefaultAutoBindDLQ(false); properties.clear(); properties.put("prefix", "latebinder."); MessageChannel noDLQOutputChannel = new DirectChannel(); - binder.bindProducer("lateNoDLQ.0", noDLQOutputChannel, properties); + Binding noDlqProducerBinding = binder.bindProducer("lateNoDLQ.0", noDLQOutputChannel, properties); QueueChannel noDLQInputChannel = new QueueChannel(); - binder.bindConsumer("lateNoDLQ.0", noDLQInputChannel, properties); + Binding noDlqConsumerBinding = binder.bindConsumer("lateNoDLQ.0", "test", noDLQInputChannel, properties); - MessageChannel pubSubOutputChannel = new DirectChannel(); - binder.bindPubSubProducer("latePubSub", pubSubOutputChannel, properties); + MessageChannel outputChannel = new DirectChannel(); + Binding pubSubProducerBinding = binder.bindProducer("latePubSub", outputChannel, properties); QueueChannel pubSubInputChannel = new QueueChannel(); - binder.bindPubSubConsumer("latePubSub", pubSubInputChannel, "lategroup", properties); + Binding nonDurableConsumerBinding = binder.bindConsumer("latePubSub", "lategroup", pubSubInputChannel, properties); QueueChannel durablePubSubInputChannel = new QueueChannel(); properties.setProperty("durableSubscription", "true"); - binder.bindPubSubConsumer("latePubSub", durablePubSubInputChannel, "lateDurableGroup", properties); + Binding durableConsumerBinding = binder.bindConsumer("latePubSub", "lateDurableGroup", durablePubSubInputChannel, properties); proxy.start(); @@ -701,7 +669,7 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { assertNotNull(message); assertEquals("bar", message.getPayload()); - pubSubOutputChannel.send(new GenericMessage<>("baz")); + outputChannel.send(new GenericMessage<>("baz")); message = pubSubInputChannel.receive(10000); assertNotNull(message); assertEquals("baz", message.getPayload()); @@ -718,28 +686,22 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { assertNotNull(message); assertEquals("1", message.getPayload()); - binder.unbindProducer("late.0", moduleOutputChannel); - binder.unbindConsumer("late.0", moduleInputChannel); - binder.unbindProducer("partlate.0", moduleOutputChannel); - binder.unbindConsumers("partlate.0"); + binder.unbind(late0ProducerBinding); + binder.unbind(late0ConsumerBinding); + binder.unbind(partlate0ProducerBinding); + binder.unbind(partlate0Consumer0Binding); + binder.unbind(partlate0Consumer1Binding); + binder.unbind(noDlqProducerBinding); + binder.unbind(noDlqConsumerBinding); + binder.unbind(pubSubProducerBinding); + binder.unbind(nonDurableConsumerBinding); + binder.unbind(durableConsumerBinding); + + binder.cleanup(); proxy.stop(); cf.destroy(); - RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource()); - admin.deleteQueue("latebinder.late.0"); - admin.deleteQueue("latebinder.lateNoDLQ.0"); - admin.deleteQueue("latebinder.partlate.0-0"); - admin.deleteQueue("latebinder.partlate.0-1"); - admin.deleteQueue("latebinder.late.0.dlq"); - admin.deleteQueue("latebinder.partlate.0-0.dlq"); - admin.deleteQueue("latebinder.partlate.0-1.dlq"); - admin.deleteQueue("latebinder.lateDurableGroup.latePubSub"); - admin.deleteExchange("latebinder.late.0"); - admin.deleteExchange("latebinder.lateNoDLQ.0"); - admin.deleteExchange("latebinder.partlate.0"); - admin.deleteExchange("latebinder.latePubSub"); - admin.deleteExchange("latebinder.DLX"); this.rabbitAvailableRule.getResource().destroy(); } @@ -772,26 +734,6 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { return container; } - private void verifyBarReplyConsumer(AbstractEndpoint endpoint) { - List replyMatchers; - replyMatchers = TestUtils.getPropertyValue(endpoint, - "headerMapper.replyHeaderMatcher.strategies", - List.class); - assertEquals(1, replyMatchers.size()); - assertEquals("bar", - TestUtils.getPropertyValue(replyMatchers.get(0), "patterns", Collection.class).iterator().next()); - } - - private void verifyFooRequestBarReplyProducer(AbstractEndpoint endpoint) { - verifyFooRequestProducer(endpoint); - List replyMatchers = TestUtils.getPropertyValue(endpoint, - "handler.delegate.headerMapper.replyHeaderMatcher.strategies", - List.class); - assertEquals(1, replyMatchers.size()); - assertEquals("bar", - TestUtils.getPropertyValue(replyMatchers.get(0), "patterns", Collection.class).iterator().next()); - } - private void verifyFooRequestProducer(AbstractEndpoint endpoint) { List requestMatchers = TestUtils.getPropertyValue(endpoint, "handler.delegate.headerMapper.requestHeaderMatcher.strategies", @@ -807,6 +749,11 @@ public class RabbitBinderTests extends PartitionCapableBinderTests { SpelExpression.class).getExpressionString(); } + @Override + protected String getExpectedRoutingBaseDestination(String name, String group) { + return name; + } + @Override protected String getPubSubEndpointRouting(AbstractEndpoint endpoint) { return TestUtils.getPropertyValue(endpoint, "handler.delegate.exchangeNameExpression", diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitTestBinder.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitTestBinder.java index 0d80496c0..65651b88d 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitTestBinder.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/RabbitTestBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -23,19 +23,20 @@ import java.util.Set; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.cloud.stream.binder.AbstractTestBinder; +import org.springframework.cloud.stream.binder.Binding; import org.springframework.context.support.GenericApplicationContext; import org.springframework.integration.codec.kryo.PojoCodec; import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.messaging.MessageChannel; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; - /** * Test support class for {@link RabbitMessageChannelBinder}. * * @author Ilayaperumal Gopinathan * @author Gary Russell * @author David Turanski + * @author Mark Fisher */ public class RabbitTestBinder extends AbstractTestBinder { @@ -45,8 +46,15 @@ public class RabbitTestBinder extends AbstractTestBinder prefixes = new HashSet<>(); + private final Set queues = new HashSet(); + + private final Set exchanges = new HashSet(); + public RabbitTestBinder(ConnectionFactory connectionFactory) { - RabbitMessageChannelBinder binder = new RabbitMessageChannelBinder(connectionFactory); + this(connectionFactory, new RabbitMessageChannelBinder(connectionFactory)); + } + + public RabbitTestBinder(ConnectionFactory connectionFactory, RabbitMessageChannelBinder binder) { GenericApplicationContext context = new GenericApplicationContext(); ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(1); @@ -60,75 +68,48 @@ public class RabbitTestBinder extends AbstractTestBinder bindConsumer(String name, String group, MessageChannel moduleInputChannel, Properties properties) { + this.queues.add(prefix(properties) + name + (group == null ? ".default" : "." + group)); + this.exchanges.add(prefix(properties) + name); + return super.bindConsumer(name, group, moduleInputChannel, properties); + } @Override - public void bindProducer(String name, MessageChannel moduleOutputChannel, Properties properties) { - capturePrefix(properties); - super.bindProducer(name, moduleOutputChannel, properties); + public Binding bindProducer(String name, MessageChannel moduleOutputChannel, Properties properties) { + this.queues.add(prefix(properties) + name + ".default"); + this.exchanges.add(prefix(properties) + name); + return super.bindProducer(name, moduleOutputChannel, properties); } - @Override - public void bindPubSubConsumer(String name, MessageChannel inputChannel, String group, Properties properties) { - capturePrefix(properties); - super.bindPubSubConsumer(name, inputChannel, group, properties); - } - - @Override - public void bindPubSubProducer(String name, MessageChannel outputChannel, Properties properties) { - capturePrefix(properties); - super.bindPubSubProducer(name, outputChannel, properties); - } - - @Override - public void bindRequestor(String name, MessageChannel requests, MessageChannel replies, Properties properties) { - capturePrefix(properties); - super.bindRequestor(name, requests, replies, properties); - } - - @Override - public void bindReplier(String name, MessageChannel requests, MessageChannel replies, Properties properties) { - capturePrefix(properties); - super.bindReplier(name, requests, replies, properties); - } - - public void capturePrefix(Properties properties) { + public String prefix(Properties properties) { if (properties != null) { String prefix = properties.getProperty("prefix"); if (prefix != null) { this.prefixes.add(prefix); + return prefix; } } + return BINDER_PREFIX; } @Override public void cleanup() { - if (!queues.isEmpty()) { - for (String queue : queues) { - rabbitAdmin.deleteQueue(BINDER_PREFIX + queue); - // delete any partitioned queues - for (int i = 0; i < 10; i++) { - rabbitAdmin.deleteQueue(BINDER_PREFIX + queue + "-" + i); - } - for (String prefix : this.prefixes) { - rabbitAdmin.deleteQueue(prefix + queue); - // delete any partitioned queues - for (int i = 0; i < 10; i++) { - rabbitAdmin.deleteQueue(prefix + queue + "-" + i); - } - rabbitAdmin.deleteExchange(prefix + queue); - rabbitAdmin.deleteExchange(prefix + queue + ".requests"); - } - rabbitAdmin.deleteExchange(BINDER_PREFIX + queue); + for (String queue : this.queues) { + this.rabbitAdmin.deleteQueue(queue); + this.rabbitAdmin.deleteQueue(queue + ".dlq"); + // delete any partitioned queues + for (int i = 0; i < 10; i++) { + this.rabbitAdmin.deleteQueue(queue + "-" + i); + this.rabbitAdmin.deleteQueue(queue + "-" + i + ".dlq"); } } - if (!topics.isEmpty()) { - for (String exchange : topics) { - rabbitAdmin.deleteExchange(BINDER_PREFIX + exchange); - } + for (String exchange : this.exchanges) { + this.rabbitAdmin.deleteExchange(exchange); + } + for (String prefix : this.prefixes) { + this.rabbitAdmin.deleteExchange(prefix + "DLX"); } } + } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/integration/RabbitBinderModuleTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/integration/RabbitBinderModuleTests.java index bf237c426..2c693eedb 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/integration/RabbitBinderModuleTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/integration/RabbitBinderModuleTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -65,8 +65,8 @@ public class RabbitBinderModuleTests { context = null; } RabbitAdmin admin = new RabbitAdmin(rabbitTestSupport.getResource()); - admin.deleteQueue("binder.input"); - admin.deleteQueue("binder.output"); + admin.deleteQueue("binder.input.default"); + admin.deleteQueue("binder.output.default"); admin.deleteExchange("binder.input"); admin.deleteExchange("binder.output"); } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/main/java/org/springframework/cloud/stream/binder/redis/RedisMessageChannelBinder.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/main/java/org/springframework/cloud/stream/binder/redis/RedisMessageChannelBinder.java index e9eb6f7a3..6ebe5af68 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/main/java/org/springframework/cloud/stream/binder/redis/RedisMessageChannelBinder.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/main/java/org/springframework/cloud/stream/binder/redis/RedisMessageChannelBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-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. @@ -18,7 +18,9 @@ package org.springframework.cloud.stream.binder.redis; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.Set; @@ -31,6 +33,8 @@ import org.springframework.cloud.stream.binder.EmbeddedHeadersMessageConverter; import org.springframework.cloud.stream.binder.MessageChannelBinderSupport; import org.springframework.cloud.stream.binder.MessageValues; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.integration.channel.DirectChannel; @@ -38,9 +42,7 @@ import org.springframework.integration.endpoint.EventDrivenConsumer; import org.springframework.integration.endpoint.MessageProducerSupport; import org.springframework.integration.handler.AbstractMessageHandler; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; -import org.springframework.integration.redis.inbound.RedisInboundChannelAdapter; import org.springframework.integration.redis.inbound.RedisQueueMessageDrivenEndpoint; -import org.springframework.integration.redis.outbound.RedisPublishingMessageHandler; import org.springframework.integration.redis.outbound.RedisQueueOutboundChannelAdapter; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -56,6 +58,7 @@ import org.springframework.util.StringUtils; /** * A {@link org.springframework.cloud.stream.binder.Binder} implementation backed by Redis. + * * @author Mark Fisher * @author Gary Russell * @author David Turanski @@ -65,71 +68,30 @@ public class RedisMessageChannelBinder extends MessageChannelBinderSupport imple private static final String ERROR_HEADER = "errorKey"; + private static final String CONSUMER_GROUPS_KEY_PREFIX = "groups."; + private static final SpelExpressionParser parser = new SpelExpressionParser(); private final String[] headersToMap; - /** - * Retry only. - */ - private static final Set SUPPORTED_PUBSUB_CONSUMER_PROPERTIES = new SetBuilder() - .addAll(CONSUMER_STANDARD_PROPERTIES) - .addAll(CONSUMER_RETRY_PROPERTIES) - .build(); + private final RedisOperations redisOperations; /** - * Retry + concurrency. - */ - private static final Set SUPPORTED_NAMED_CONSUMER_PROPERTIES = new SetBuilder() - .addAll(CONSUMER_STANDARD_PROPERTIES) - .addAll(CONSUMER_RETRY_PROPERTIES) - .add(BinderPropertyKeys.CONCURRENCY) - .build(); - - /** - * Named + partitioning. + * Retry + concurrency + partitioning. */ private static final Set SUPPORTED_CONSUMER_PROPERTIES = new SetBuilder() - .addAll(SUPPORTED_NAMED_CONSUMER_PROPERTIES) - .add(BinderPropertyKeys.PARTITION_INDEX) - .build(); - - /** - * Retry + concurrency (request). - */ - private static final Set SUPPORTED_REPLYING_CONSUMER_PROPERTIES = new SetBuilder() - // request .addAll(CONSUMER_STANDARD_PROPERTIES) .addAll(CONSUMER_RETRY_PROPERTIES) .add(BinderPropertyKeys.CONCURRENCY) + .add(BinderPropertyKeys.PARTITION_INDEX) .build(); - /** - * None. - */ - private static final Set SUPPORTED_PUBSUB_PRODUCER_PROPERTIES = PRODUCER_STANDARD_PROPERTIES; - - /** - * None. - */ - private static final Set SUPPORTED_NAMED_PRODUCER_PROPERTIES = PRODUCER_STANDARD_PROPERTIES; - /** * Partitioning. */ private static final Set SUPPORTED_PRODUCER_PROPERTIES = new SetBuilder() .addAll(PRODUCER_PARTITIONING_PROPERTIES) .addAll(PRODUCER_STANDARD_PROPERTIES) - .add(BinderPropertyKeys.DIRECT_BINDING_ALLOWED) - .build(); - - /** - * Retry, concurrency (reply). - */ - private static final Set SUPPORTED_REQUESTING_PRODUCER_PROPERTIES = new SetBuilder() - // reply - .addAll(CONSUMER_RETRY_PROPERTIES) - .add(BinderPropertyKeys.CONCURRENCY) .build(); private final RedisConnectionFactory connectionFactory; @@ -143,11 +105,12 @@ public class RedisMessageChannelBinder extends MessageChannelBinderSupport imple this(connectionFactory, new String[0]); } - public RedisMessageChannelBinder(RedisConnectionFactory connectionFactory, - String... headersToMap) { + public RedisMessageChannelBinder(RedisConnectionFactory connectionFactory, String... headersToMap) { Assert.notNull(connectionFactory, "connectionFactory must not be null"); this.connectionFactory = connectionFactory; - + StringRedisTemplate template = new StringRedisTemplate(connectionFactory); + template.afterPropertiesSet(); + this.redisOperations = template; if (headersToMap != null && headersToMap.length > 0) { String[] combinedHeadersToMap = Arrays.copyOfRange(BinderHeaders.STANDARD_HEADERS, 0, BinderHeaders.STANDARD_HEADERS.length @@ -159,7 +122,6 @@ public class RedisMessageChannelBinder extends MessageChannelBinderSupport imple else { this.headersToMap = BinderHeaders.STANDARD_HEADERS; } - this.errorAdapter = new RedisQueueOutboundChannelAdapter( parser.parseExpression("headers['" + ERROR_HEADER + "']"), connectionFactory); } @@ -173,23 +135,16 @@ public class RedisMessageChannelBinder extends MessageChannelBinderSupport imple } @Override - public void bindConsumer(final String name, MessageChannel moduleInputChannel, Properties properties) { - if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX)) { - validateConsumerProperties(name, properties, SUPPORTED_NAMED_CONSUMER_PROPERTIES); - } - else { - validateConsumerProperties(name, properties, SUPPORTED_CONSUMER_PROPERTIES); - } + protected Binding doBindConsumer(final String name, String group, MessageChannel moduleInputChannel, Properties properties) { RedisPropertiesAccessor accessor = new RedisPropertiesAccessor(properties); - String queueName = "queue." + name; + String queueName = groupedName(name, group); + validateConsumerProperties(queueName, properties, SUPPORTED_CONSUMER_PROPERTIES); int partitionIndex = accessor.getPartitionIndex(); if (partitionIndex >= 0) { queueName += "-" + partitionIndex; } MessageProducerSupport adapter = createInboundAdapter(accessor, queueName); - doRegisterConsumer(name, name + (partitionIndex >= 0 ? "-" + partitionIndex : ""), moduleInputChannel, adapter, - accessor); - bindExistingProducerDirectlyIfPossible(name, moduleInputChannel); + return doRegisterConsumer(name, group, queueName, moduleInputChannel, adapter, accessor); } private MessageProducerSupport createInboundAdapter(RedisPropertiesAccessor accessor, String queueName) { @@ -209,37 +164,25 @@ public class RedisMessageChannelBinder extends MessageChannelBinderSupport imple return adapter; } - @Override - public void bindPubSubConsumer(final String name, MessageChannel moduleInputChannel, String group, - Properties properties) { - if (logger.isInfoEnabled()) { - logger.info("declaring pubsub for inbound: " + name); - } - validateConsumerProperties(name, properties, SUPPORTED_PUBSUB_CONSUMER_PROPERTIES); - RedisInboundChannelAdapter adapter = new RedisInboundChannelAdapter(this.connectionFactory); - adapter.setBeanFactory(this.getBeanFactory()); - adapter.setSerializer(null); - adapter.setTopics(applyPubSub(name)); - doRegisterConsumer(name, name, moduleInputChannel, adapter, new RedisPropertiesAccessor(properties)); - } - - private void doRegisterConsumer(String bindingName, String channelName, MessageChannel moduleInputChannel, + private Binding doRegisterConsumer(String bindingName, String group, String channelName, MessageChannel moduleInputChannel, MessageProducerSupport adapter, RedisPropertiesAccessor properties) { DirectChannel bridgeToModuleChannel = new DirectChannel(); bridgeToModuleChannel.setBeanFactory(this.getBeanFactory()); bridgeToModuleChannel.setBeanName(channelName + ".bridge"); MessageChannel bridgeInputChannel = addRetryIfNeeded(channelName, bridgeToModuleChannel, properties); adapter.setOutputChannel(bridgeInputChannel); - adapter.setBeanName("inbound." + bindingName); + adapter.setBeanName("inbound." + channelName); adapter.afterPropertiesSet(); - Binding consumerBinding = Binding.forConsumer(bindingName, adapter, moduleInputChannel, properties); + Binding consumerBinding = Binding.forConsumer(channelName, group, adapter, moduleInputChannel, properties); addBinding(consumerBinding); ReceivingHandler convertingBridge = new ReceivingHandler(); convertingBridge.setOutputChannel(moduleInputChannel); convertingBridge.setBeanName(channelName + ".bridge.handler"); convertingBridge.afterPropertiesSet(); bridgeToModuleChannel.subscribe(convertingBridge); + this.redisOperations.boundZSetOps(CONSUMER_GROUPS_KEY_PREFIX + bindingName).incrementScore(group, 1); consumerBinding.start(); + return consumerBinding; } /** @@ -300,101 +243,49 @@ public class RedisMessageChannelBinder extends MessageChannelBinderSupport imple } @Override - public void bindProducer(final String name, MessageChannel moduleOutputChannel, - Properties properties) { - Assert.isInstanceOf(SubscribableChannel.class, moduleOutputChannel); - if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX)) { - validateProducerProperties(name, properties, SUPPORTED_NAMED_PRODUCER_PROPERTIES); - } - else { - validateProducerProperties(name, properties, SUPPORTED_PRODUCER_PROPERTIES); - } - RedisPropertiesAccessor accessor = new RedisPropertiesAccessor(properties); - if (!bindNewProducerDirectlyIfPossible(name, (SubscribableChannel) moduleOutputChannel, accessor)) { - String partitionKeyExtractorClass = accessor.getPartitionKeyExtractorClass(); - Expression partitionKeyExpression = accessor.getPartitionKeyExpression(); - RedisQueueOutboundChannelAdapter queue; - String queueName = "queue." + name; - if (partitionKeyExpression == null && !StringUtils.hasText(partitionKeyExtractorClass)) { - queue = new RedisQueueOutboundChannelAdapter(queueName, this.connectionFactory); - } - else { - queue = new RedisQueueOutboundChannelAdapter( - parser.parseExpression(buildPartitionRoutingExpression(queueName)), this.connectionFactory); - } - queue.setIntegrationEvaluationContext(this.evaluationContext); - queue.setBeanFactory(this.getBeanFactory()); - queue.afterPropertiesSet(); - doRegisterProducer(name, moduleOutputChannel, queue, accessor); + protected void afterUnbind(Binding binding) { + if (Binding.Type.consumer.equals(binding.getType())) { + String key = CONSUMER_GROUPS_KEY_PREFIX + binding.getName(); + this.redisOperations.boundZSetOps(key).incrementScore(binding.getGroup(), -1); } } @Override - public void bindPubSubProducer(final String name, MessageChannel moduleOutputChannel, - Properties properties) { - validateProducerProperties(name, properties, SUPPORTED_PUBSUB_PRODUCER_PROPERTIES); - RedisPublishingMessageHandler topic = new RedisPublishingMessageHandler(connectionFactory); - topic.setBeanFactory(this.getBeanFactory()); - topic.setTopic(applyPubSub(name)); - topic.afterPropertiesSet(); - doRegisterProducer(name, moduleOutputChannel, topic, new RedisPropertiesAccessor(properties)); - } - - private void doRegisterProducer(final String name, MessageChannel moduleOutputChannel, MessageHandler delegate, - RedisPropertiesAccessor properties) { - this.doRegisterProducer(name, moduleOutputChannel, delegate, null, properties); - } - - private void doRegisterProducer(final String name, MessageChannel moduleOutputChannel, MessageHandler delegate, - String replyTo, RedisPropertiesAccessor properties) { + public Binding bindProducer(final String name, MessageChannel moduleOutputChannel, Properties properties) { Assert.isInstanceOf(SubscribableChannel.class, moduleOutputChannel); - MessageHandler handler = new SendingHandler(delegate, replyTo, properties); + validateProducerProperties(name, properties, SUPPORTED_PRODUCER_PROPERTIES); + RedisPropertiesAccessor accessor = new RedisPropertiesAccessor(properties); + return doRegisterProducer(name, moduleOutputChannel, accessor); + } + + private RedisQueueOutboundChannelAdapter createProducerEndpoint(String name, RedisPropertiesAccessor accessor) { + String partitionKeyExtractorClass = accessor.getPartitionKeyExtractorClass(); + Expression partitionKeyExpression = accessor.getPartitionKeyExpression(); + RedisQueueOutboundChannelAdapter queue; + if (partitionKeyExpression == null && !StringUtils.hasText(partitionKeyExtractorClass)) { + queue = new RedisQueueOutboundChannelAdapter(name, this.connectionFactory); + } + else { + queue = new RedisQueueOutboundChannelAdapter( + parser.parseExpression(buildPartitionRoutingExpression(name)), this.connectionFactory); + } + queue.setIntegrationEvaluationContext(this.evaluationContext); + queue.setBeanFactory(this.getBeanFactory()); + queue.afterPropertiesSet(); + return queue; + } + + private Binding doRegisterProducer(final String name, MessageChannel moduleOutputChannel, RedisPropertiesAccessor properties) { + Assert.isInstanceOf(SubscribableChannel.class, moduleOutputChannel); + MessageHandler handler = new SendingHandler(name, properties); EventDrivenConsumer consumer = new EventDrivenConsumer((SubscribableChannel) moduleOutputChannel, handler); consumer.setBeanFactory(this.getBeanFactory()); consumer.setBeanName("outbound." + name); consumer.afterPropertiesSet(); - Binding producerBinding = Binding.forProducer(name, moduleOutputChannel, consumer, properties); + Binding producerBinding = Binding.forProducer(name, moduleOutputChannel, consumer, properties); addBinding(producerBinding); producerBinding.start(); - } - - @Override - public void bindRequestor(String name, MessageChannel requests, MessageChannel replies, - Properties properties) { - if (logger.isInfoEnabled()) { - logger.info("binding requestor: " + name); - } - Assert.isInstanceOf(SubscribableChannel.class, requests); - validateProducerProperties(name, properties, SUPPORTED_REQUESTING_PRODUCER_PROPERTIES); - RedisQueueOutboundChannelAdapter queue = new RedisQueueOutboundChannelAdapter("queue." + applyRequests(name), - this.connectionFactory); - queue.setBeanFactory(this.getBeanFactory()); - queue.afterPropertiesSet(); - String replyQueueName = name + ".replies." + this.getIdGenerator().generateId(); - RedisPropertiesAccessor accessor = new RedisPropertiesAccessor(properties); - this.doRegisterProducer(name, requests, queue, replyQueueName, accessor); - MessageProducerSupport adapter = createInboundAdapter(accessor, replyQueueName); - this.doRegisterConsumer(name, name, replies, adapter, accessor); - } - - @Override - public void bindReplier(String name, MessageChannel requests, MessageChannel replies, - Properties properties) { - if (logger.isInfoEnabled()) { - logger.info("binding replier: " + name); - } - validateConsumerProperties(name, properties, SUPPORTED_REPLYING_CONSUMER_PROPERTIES); - RedisPropertiesAccessor accessor = new RedisPropertiesAccessor(properties); - MessageProducerSupport adapter = createInboundAdapter(accessor, "queue." + applyRequests(name)); - this.doRegisterConsumer(name, name, requests, adapter, accessor); - - RedisQueueOutboundChannelAdapter replyQueue = new RedisQueueOutboundChannelAdapter( - RedisMessageChannelBinder.parser.parseExpression("headers['" + BinderHeaders.REPLY_TO + "']"), - this.connectionFactory); - replyQueue.setBeanFactory(this.getBeanFactory()); - replyQueue.setIntegrationEvaluationContext(this.evaluationContext); - replyQueue.afterPropertiesSet(); - this.doRegisterProducer(name, replies, replyQueue, accessor); + return producerBinding; } @Override @@ -404,37 +295,48 @@ public class RedisMessageChannelBinder extends MessageChannelBinderSupport imple private class SendingHandler extends AbstractMessageHandler { - private final MessageHandler delegate; - - private final String replyTo; + private final String bindingName; private final PartitioningMetadata partitioningMetadata; + private final RedisPropertiesAccessor accessor; - private SendingHandler(MessageHandler delegate, String replyTo, RedisPropertiesAccessor properties) { - this.delegate = delegate; - this.replyTo = replyTo; + private final Map adapters = new HashMap<>(); + + private SendingHandler(String bindingName, RedisPropertiesAccessor properties) { + this.bindingName = bindingName; + this.accessor = properties; this.partitioningMetadata = new PartitioningMetadata(properties, properties.getNextModuleCount()); this.setBeanFactory(RedisMessageChannelBinder.this.getBeanFactory()); + refreshChannelAdapters(); } @Override protected void handleMessageInternal(Message message) throws Exception { MessageValues transformed = serializePayloadIfNecessary(message); - if (replyTo != null) { - transformed.put(BinderHeaders.REPLY_TO, this.replyTo); - } if (this.partitioningMetadata.isPartitionedModule()) { - transformed.put(PARTITION_HEADER, determinePartition(message, this.partitioningMetadata)); } byte[] messageToSend = embeddedHeadersMessageConverter.embedHeaders(transformed, RedisMessageChannelBinder.this.headersToMap); - delegate.handleMessage(MessageBuilder.withPayload(messageToSend).copyHeaders(transformed).build()); + + refreshChannelAdapters(); + for (RedisQueueOutboundChannelAdapter adapter : adapters.values()) { + adapter.handleMessage((MessageBuilder.withPayload(messageToSend).copyHeaders(transformed).build())); + } } + private void refreshChannelAdapters() { + Set groups = redisOperations.boundZSetOps(CONSUMER_GROUPS_KEY_PREFIX + bindingName).rangeByScore(1, Double.MAX_VALUE); + for (String group : groups) { + if (!adapters.containsKey(group)) { + String channel = String.format("%s.%s", this.bindingName, group); + adapters.put(group, createProducerEndpoint(channel, accessor)); + } + } + } } private class ReceivingHandler extends AbstractReplyProducingMessageHandler { diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/test/java/org/springframework/cloud/stream/binder/redis/RedisBinderTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/test/java/org/springframework/cloud/stream/binder/redis/RedisBinderTests.java index 50f052f57..367f548c6 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/test/java/org/springframework/cloud/stream/binder/redis/RedisBinderTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/test/java/org/springframework/cloud/stream/binder/redis/RedisBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-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. @@ -16,7 +16,6 @@ package org.springframework.cloud.stream.binder.redis; -import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; @@ -24,17 +23,15 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; @@ -52,6 +49,7 @@ import org.springframework.expression.Expression; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.endpoint.AbstractEndpoint; import org.springframework.integration.redis.inbound.RedisQueueMessageDrivenEndpoint; +import org.springframework.integration.redis.outbound.RedisQueueOutboundChannelAdapter; import org.springframework.integration.test.util.TestUtils; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.support.GenericMessage; @@ -60,6 +58,7 @@ import org.springframework.retry.support.RetryTemplate; /** * @author Gary Russell * @author David Turanski + * @author Mark Fisher */ public class RedisBinderTests extends PartitionCapableBinderTests { @@ -86,34 +85,20 @@ public class RedisBinderTests extends PartitionCapableBinderTests { return true; } - @Override - @Test - @Ignore("https://github.com/spring-cloud/spring-cloud-stream/issues/247") - public void testSendAndReceivePubSub() throws Exception { - - //TimeUnit.SECONDS.sleep(2); - - super.testSendAndReceivePubSub(); - } - - @Before - public void setup() { - createTemplate().boundListOps("queue.direct.0").trim(1, 0); - } - @Test public void testConsumerProperties() throws Exception { Binder binder = getBinder(); Properties properties = new Properties(); properties.put("maxAttempts", "1"); // disable retry - binder.bindConsumer("props.0", new DirectChannel(), properties); + Binding binding = binder.bindConsumer("props.0", "test", new DirectChannel(), properties); @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + List> bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); assertEquals(1, bindings.size()); - AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); + assertEquals(binding, bindings.get(0)); + AbstractEndpoint endpoint = binding.getEndpoint(); assertThat(endpoint, instanceOf(RedisQueueMessageDrivenEndpoint.class)); assertSame(DirectChannel.class, TestUtils.getPropertyValue(endpoint, "outputChannel").getClass()); - binder.unbindConsumers("props.0"); + binder.unbind(binding); assertEquals(0, bindings.size()); properties.put("backOffInitialInterval", "2000"); @@ -123,48 +108,34 @@ public class RedisBinderTests extends PartitionCapableBinderTests { properties.put("maxAttempts", "23"); properties.put("partitionIndex", 0); - binder.bindConsumer("props.0", new DirectChannel(), properties); + binding = binder.bindConsumer("props.0", "test", new DirectChannel(), properties); assertEquals(1, bindings.size()); - endpoint = bindings.get(0).getEndpoint(); + assertEquals(binding, bindings.get(0)); + endpoint = binding.getEndpoint(); verifyConsumer(endpoint); - try { - binder.bindPubSubConsumer("dummy", null, null, properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support consumer properties: "), - containsString("partitionIndex"), - containsString("concurrency"), - containsString(" for dummy."))); - } - try { - binder.bindConsumer("queue:dummy", null, properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertEquals(getClassUnderTestName() + " does not support consumer property: partitionIndex for queue:dummy.", - e.getMessage()); - } - - binder.unbindConsumers("props.0"); + binder.unbind(binding); assertEquals(0, bindings.size()); } @Test public void testProducerProperties() throws Exception { Binder binder = getBinder(); - binder.bindProducer("props.0", new DirectChannel(), null); + Binding consumerBinding = binder.bindConsumer("props.0", "test", new DirectChannel(), null); + Binding producerBinding = binder.bindProducer("props.0", new DirectChannel(), null); @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); - assertEquals(1, bindings.size()); - AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); + List> bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + assertEquals(2, bindings.size()); + assertEquals(producerBinding, bindings.get(1)); + AbstractEndpoint endpoint = producerBinding.getEndpoint(); + @SuppressWarnings("unchecked") + Map adapters = TestUtils.getPropertyValue(endpoint, "handler.adapters", Map.class); + RedisQueueOutboundChannelAdapter adapter = adapters.get("test"); assertEquals( - "queue.props.0", - TestUtils.getPropertyValue(endpoint, "handler.delegate.queueNameExpression", Expression.class).getExpressionString()); - binder.unbindProducers("props.0"); - assertEquals(0, bindings.size()); + "props.0.test", + TestUtils.getPropertyValue(adapter, "queueNameExpression", Expression.class).getExpressionString()); + binder.unbind(producerBinding); + assertEquals(1, bindings.size()); Properties properties = new Properties(); properties.put("partitionKeyExpression", "'foo'"); @@ -173,138 +144,16 @@ public class RedisBinderTests extends PartitionCapableBinderTests { properties.put("partitionSelectorClass", "foo"); properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "1"); - binder.bindProducer("props.0", new DirectChannel(), properties); - assertEquals(1, bindings.size()); - endpoint = bindings.get(0).getEndpoint(); - assertEquals( - "'queue.props.0-' + headers['partition']", - TestUtils.getPropertyValue(endpoint, "handler.delegate.queueNameExpression", Expression.class).getExpressionString()); - - try { - binder.bindPubSubProducer("dummy", null, properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support producer properties: "), - containsString("partitionSelectorExpression"), - containsString("partitionKeyExtractorClass"), - containsString("partitionKeyExpression"), - containsString("partitionSelectorClass"))); - assertThat(e.getMessage(), containsString("for dummy.")); - } - try { - binder.bindProducer("queue:dummy", new DirectChannel(), properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support producer properties: "), - containsString("partitionSelectorExpression"), - containsString("partitionKeyExtractorClass"), - containsString("partitionKeyExpression"), - containsString("partitionSelectorClass"))); - assertThat(e.getMessage(), containsString("for queue:dummy.")); - } - - binder.unbindProducers("props.0"); - assertEquals(0, bindings.size()); - } - - @Test - public void testRequestReplyRequestorProperties() throws Exception { - Binder binder = getBinder(); - Properties properties = new Properties(); - - properties.put("backOffInitialInterval", "2000"); - properties.put("backOffMaxInterval", "20000"); - properties.put("backOffMultiplier", "5.0"); - properties.put("concurrency", "2"); - properties.put("maxAttempts", "23"); - - binder.bindRequestor("props.0", new DirectChannel(), new DirectChannel(), properties); - @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); - + producerBinding = binder.bindProducer("props.0", new DirectChannel(), properties); assertEquals(2, bindings.size()); - AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); // producer + endpoint = bindings.get(1).getEndpoint(); + adapter = (RedisQueueOutboundChannelAdapter) TestUtils.getPropertyValue(endpoint, "handler.adapters", Map.class).get("test"); assertEquals( - "queue.props.0.requests", - TestUtils.getPropertyValue(endpoint, "handler.delegate.queueNameExpression", Expression.class).getExpressionString()); + "'props.0.test-' + headers['partition']", + TestUtils.getPropertyValue(adapter, "queueNameExpression", Expression.class).getExpressionString()); - endpoint = bindings.get(1).getEndpoint(); // consumer - verifyConsumer(endpoint); - - properties.put("partitionKeyExpression", "'foo'"); - properties.put("partitionKeyExtractorClass", "foo"); - properties.put("partitionSelectorExpression", "0"); - properties.put("partitionSelectorClass", "foo"); - properties.put("partitionIndex", "0"); - try { - binder.bindRequestor("dummy", new DirectChannel(), new DirectChannel(), properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support producer properties: "), - containsString("partitionSelectorExpression"), - containsString("partitionKeyExtractorClass"), - containsString("partitionKeyExpression"), - containsString("partitionSelectorClass"))); - assertThat(e.getMessage(), allOf(containsString("partitionIndex"), containsString("for dummy."))); - } - - binder.unbindConsumers("props.0"); - binder.unbindProducers("props.0"); - assertEquals(0, bindings.size()); - } - - @Test - public void testRequestReplyReplierProperties() throws Exception { - Binder binder = getBinder(); - Properties properties = new Properties(); - - properties.put("backOffInitialInterval", "2000"); - properties.put("backOffMaxInterval", "20000"); - properties.put("backOffMultiplier", "5.0"); - properties.put("concurrency", "2"); - properties.put("maxAttempts", "23"); - - binder.bindReplier("props.0", new DirectChannel(), new DirectChannel(), properties); - @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); - - assertEquals(2, bindings.size()); - AbstractEndpoint endpoint = bindings.get(1).getEndpoint(); // producer - assertEquals( - "headers['replyTo']", - TestUtils.getPropertyValue(endpoint, "handler.delegate.queueNameExpression", Expression.class).getExpressionString()); - - endpoint = bindings.get(0).getEndpoint(); // consumer - verifyConsumer(endpoint); - - properties.put("partitionKeyExpression", "'foo'"); - properties.put("partitionKeyExtractorClass", "foo"); - properties.put("partitionSelectorExpression", "0"); - properties.put("partitionSelectorClass", "foo"); - properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "1"); - properties.put("partitionIndex", "0"); - try { - binder.bindReplier("dummy", new DirectChannel(), new DirectChannel(), properties); - fail("Expected exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage(), allOf( - containsString(getClassUnderTestName() + " does not support consumer properties: "), - containsString("partitionSelectorExpression"), - containsString("partitionKeyExtractorClass"), - containsString("partitionKeyExpression"), - containsString("partitionSelectorClass"))); - assertThat(e.getMessage(), allOf(containsString("partitionIndex"), containsString("for dummy."))); - } - - binder.unbindConsumers("props.0"); - binder.unbindProducers("props.0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); assertEquals(0, bindings.size()); } @@ -335,12 +184,13 @@ public class RedisBinderTests extends PartitionCapableBinderTests { props.put("maxAttempts", 2); props.put("backOffInitialInterval", 100); props.put("backOffMultiplier", "1.0"); - binder.bindConsumer("retry.0", new DirectChannel(), props); // no subscriber + Binding consumerBinding = binder.bindConsumer("retry.0", "test", new DirectChannel(), props); // no subscriber channel.send(new GenericMessage("foo")); RedisTemplate template = createTemplate(); - Object rightPop = template.boundListOps("ERRORS:retry.0").rightPop(5, TimeUnit.SECONDS); + Object rightPop = template.boundListOps("ERRORS:retry.0.test").rightPop(5, TimeUnit.SECONDS); assertNotNull(rightPop); assertThat(new String((byte[]) rightPop), containsString("foo")); + binder.unbind(consumerBinding); } @Test @@ -352,10 +202,6 @@ public class RedisBinderTests extends PartitionCapableBinderTests { assertTrue(headers.contains("bar")); } - @Override @Ignore("https://github.com/spring-cloud/spring-cloud-stream/issues/247") - public void createInboundPubSubBeforeOutboundPubSub() throws Exception { - } - private RedisTemplate createTemplate() { if (this.redisTemplate != null) { return this.redisTemplate; @@ -370,13 +216,15 @@ public class RedisBinderTests extends PartitionCapableBinderTests { } @Override + @SuppressWarnings("unchecked") protected String getEndpointRouting(AbstractEndpoint endpoint) { - return TestUtils.getPropertyValue(endpoint, "handler.delegate.queueNameExpression", Expression.class).getExpressionString(); + Map adapters = TestUtils.getPropertyValue(endpoint, "handler.adapters", Map.class); + return TestUtils.getPropertyValue(adapters.values().iterator().next(), "queueNameExpression", Expression.class).getExpressionString(); } @Override - protected String getPubSubEndpointRouting(AbstractEndpoint endpoint) { - return TestUtils.getPropertyValue(endpoint, "handler.delegate.topicExpression", Expression.class).getExpressionString(); + protected String getExpectedRoutingBaseDestination(String name, String group) { + return name + "." + group; } @Override @@ -391,7 +239,7 @@ public class RedisBinderTests extends PartitionCapableBinderTests { @Override public Object receive(boolean expectNull) throws Exception { - byte[] bytes = (byte[]) template.boundListOps("queue." + queue).rightPop(50, TimeUnit.MILLISECONDS); + byte[] bytes = (byte[]) template.boundListOps(queue).rightPop(50, TimeUnit.MILLISECONDS); if (bytes == null) { return null; } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/test/java/org/springframework/cloud/stream/binder/redis/RedisTestBinder.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/test/java/org/springframework/cloud/stream/binder/redis/RedisTestBinder.java index 3da99192c..d3fc05276 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/test/java/org/springframework/cloud/stream/binder/redis/RedisTestBinder.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-redis/src/test/java/org/springframework/cloud/stream/binder/redis/RedisTestBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-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. @@ -16,18 +16,16 @@ package org.springframework.cloud.stream.binder.redis; +import org.springframework.cloud.stream.binder.AbstractTestBinder; import org.springframework.context.support.GenericApplicationContext; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.integration.channel.DefaultHeaderChannelRegistry; -import org.springframework.integration.codec.Codec; import org.springframework.integration.codec.kryo.PojoCodec; import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.integration.support.DefaultMessageBuilderFactory; import org.springframework.integration.support.utils.IntegrationUtils; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.cloud.stream.binder.AbstractTestBinder; - /** * Test support class for {@link RedisMessageChannelBinder}. diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/AbstractBinderTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/AbstractBinderTests.java index 36ffa8950..98dc1f3dc 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/AbstractBinderTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/AbstractBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-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. @@ -17,19 +17,15 @@ package org.springframework.cloud.stream.binder; 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.junit.Assert.fail; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Properties; import org.junit.After; -import org.junit.Assert; import org.junit.Test; import org.springframework.beans.DirectFieldAccessor; @@ -45,6 +41,7 @@ import org.springframework.messaging.MessageHeaders; * @author Gary Russell * @author Ilayaperumal Gopinathan * @author David Turanski + * @author Mark Fisher */ public abstract class AbstractBinderTests { @@ -55,20 +52,20 @@ public abstract class AbstractBinderTests { @Test public void testClean() throws Exception { Binder binder = getBinder(); - binder.bindProducer("foo.0", new DirectChannel(), null); - binder.bindConsumer("foo.0", new DirectChannel(), null); - binder.bindProducer("foo.1", new DirectChannel(), null); - binder.bindConsumer("foo.1", new DirectChannel(), null); - binder.bindProducer("foo.2", new DirectChannel(), null); + Binding foo0ProducerBinding = binder.bindProducer("foo.0", new DirectChannel(), null); + Binding foo0ConsumerBinding = binder.bindConsumer("foo.0", "test", new DirectChannel(), null); + Binding foo1ProducerBinding = binder.bindProducer("foo.1", new DirectChannel(), null); + Binding foo1ConsumerBinding = binder.bindConsumer("foo.1", "test", new DirectChannel(), null); + Binding foo2ProducerBinding = binder.bindProducer("foo.2", new DirectChannel(), null); Collection bindings = getBindings(binder); assertEquals(5, bindings.size()); - binder.unbindProducers("foo.0"); + binder.unbind(foo0ProducerBinding); assertEquals(4, bindings.size()); - binder.unbindConsumers("foo.0"); - binder.unbindProducers("foo.1"); + binder.unbind(foo0ConsumerBinding); + binder.unbind(foo1ProducerBinding); assertEquals(2, bindings.size()); - binder.unbindConsumers("foo.1"); - binder.unbindProducers("foo.2"); + binder.unbind(foo1ConsumerBinding); + binder.unbind(foo2ProducerBinding); assertTrue(bindings.isEmpty()); } @@ -77,8 +74,8 @@ public abstract class AbstractBinderTests { Binder binder = getBinder(); DirectChannel moduleOutputChannel = new DirectChannel(); QueueChannel moduleInputChannel = new QueueChannel(); - binder.bindProducer("foo.0", moduleOutputChannel, null); - binder.bindConsumer("foo.0", moduleInputChannel, null); + Binding producerBinding = binder.bindProducer("foo.0", moduleOutputChannel, null); + Binding consumerBinding = binder.bindConsumer("foo.0", "test", moduleInputChannel, null); Message message = MessageBuilder.withPayload("foo").setHeader(MessageHeaders.CONTENT_TYPE, "foo/bar").build(); // Let the consumer actually bind to the producer before sending a msg @@ -89,8 +86,8 @@ public abstract class AbstractBinderTests { assertEquals("foo", inbound.getPayload()); assertNull(inbound.getHeaders().get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); assertEquals("foo/bar", inbound.getHeaders().get(MessageHeaders.CONTENT_TYPE)); - binder.unbindProducers("foo.0"); - binder.unbindConsumers("foo.0"); + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } @Test @@ -98,8 +95,8 @@ public abstract class AbstractBinderTests { Binder binder = getBinder(); DirectChannel moduleOutputChannel = new DirectChannel(); QueueChannel moduleInputChannel = new QueueChannel(); - binder.bindProducer("bar.0", moduleOutputChannel, null); - binder.bindConsumer("bar.0", moduleInputChannel, null); + Binding producerBinding = binder.bindProducer("bar.0", moduleOutputChannel, null); + Binding consumerBinding = binder.bindConsumer("bar.0", "test", moduleInputChannel, null); binderBindUnbindLatency(); Message message = MessageBuilder.withPayload("foo").build(); @@ -109,164 +106,13 @@ public abstract class AbstractBinderTests { assertEquals("foo", inbound.getPayload()); assertNull(inbound.getHeaders().get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); assertNull(inbound.getHeaders().get(MessageHeaders.CONTENT_TYPE)); - binder.unbindProducers("bar.0"); - binder.unbindConsumers("bar.0"); - } - - @Test - public void testSendAndReceivePubSub() throws Exception { - Binder binder = getBinder(); - DirectChannel moduleOutputChannel = new DirectChannel(); - QueueChannel moduleInputChannel = new QueueChannel(); - QueueChannel module2InputChannel = new QueueChannel(); - QueueChannel module3InputChannel = new QueueChannel(); - binder.bindProducer("baz.0", moduleOutputChannel, null); - binder.bindConsumer("baz.0", moduleInputChannel, null); - // A new module is using the tap as an input channel - String fooTapName = "baz.0"; - binder.bindPubSubConsumer(fooTapName, module2InputChannel, "tgroup1", null); - // Another new module is using tap as an input channel - String barTapName = "baz.0"; - binder.bindPubSubConsumer(barTapName, module3InputChannel, "tgroup2", null); - Message message = MessageBuilder.withPayload("foo").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", inbound.getPayload()); - assertNull(inbound.getHeaders().get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); - assertEquals("foo/bar", inbound.getHeaders().get(MessageHeaders.CONTENT_TYPE)); - 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", tapped1.getPayload()); - assertNull(tapped1.getHeaders().get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); - assertEquals("foo/bar", tapped1.getHeaders().get(MessageHeaders.CONTENT_TYPE)); - assertEquals("foo", tapped2.getPayload()); - assertNull(tapped2.getHeaders().get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); - assertEquals("foo/bar", tapped2.getHeaders().get(MessageHeaders.CONTENT_TYPE)); - } - // delete one tap stream is deleted - binder.unbindPubSubConsumers(barTapName, "tgroup2"); - Message message2 = MessageBuilder.withPayload("bar").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 - binder.unbindConsumer(fooTapName, module2InputChannel); - // Clean up as StreamPlugin would - binder.unbindConsumer("baz.0", moduleInputChannel); - binder.unbindProducer("baz.0", moduleOutputChannel); - binder.unbindPubSubConsumers(fooTapName, "tgroup1"); - assertTrue(getBindings(binder).isEmpty()); - } - - @Test - public void createInboundPubSubBeforeOutboundPubSub() throws Exception { - Binder binder = getBinder(); - DirectChannel moduleOutputChannel = new DirectChannel(); - QueueChannel moduleInputChannel = new QueueChannel(); - QueueChannel module2InputChannel = new QueueChannel(); - QueueChannel module3InputChannel = new QueueChannel(); - // Create the tap first - String fooTapName = "baz.0"; - binder.bindPubSubConsumer(fooTapName, module2InputChannel, "tgroup1", null); - - // Then create the stream - binder.bindProducer("baz.0", moduleOutputChannel, null); - binder.bindConsumer("baz.0", moduleInputChannel, null); - - // Another new module is using tap as an input channel - String barTapName = "baz.0"; - binder.bindPubSubConsumer(barTapName, module3InputChannel, "tgroup2", null); - Message message = MessageBuilder.withPayload("foo").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", inbound.getPayload()); - assertNull(inbound.getHeaders().get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); - assertEquals("foo/bar", inbound.getHeaders().get(MessageHeaders.CONTENT_TYPE)); - 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", tapped1.getPayload()); - assertNull(tapped1.getHeaders().get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); - assertEquals("foo/bar", tapped1.getHeaders().get(MessageHeaders.CONTENT_TYPE)); - assertEquals("foo", tapped2.getPayload()); - assertNull(tapped2.getHeaders().get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE)); - assertEquals("foo/bar", tapped2.getHeaders().get(MessageHeaders.CONTENT_TYPE)); - } - // delete one tap stream is deleted - binder.unbindPubSubConsumers(barTapName, "tgroup2"); - Message message2 = MessageBuilder.withPayload("bar").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 - binder.unbindConsumer(fooTapName, module2InputChannel); - // Clean up as StreamPlugin would - binder.unbindConsumer("baz.0", moduleInputChannel); - binder.unbindProducer("baz.0", moduleOutputChannel); - binder.unbindPubSubConsumers(fooTapName, "tgroup1"); - assertTrue(getBindings(binder).isEmpty()); - } - - @Test - public void testBadDynamic() throws Exception { - Properties properties = new Properties(); - properties.setProperty(BinderPropertyKeys.PARTITION_KEY_EXPRESSION, "'foo'"); - Binder binder = getBinder(); - try { - binder.bindDynamicProducer("queue:foo", properties); - fail("Exception expected"); - } - catch (BinderException mbe) { - Assert.assertEquals("Failed to bind dynamic channel 'queue:foo' with properties " + - "{partitionKeyExpression='foo'}", - mbe.getMessage()); - if (binder instanceof AbstractTestBinder) { - binder = ((AbstractTestBinder) binder).getCoreBinder(); - } - assertFalse(((MessageChannelBinderSupport) binder).getApplicationContext().containsBean("queue:foo")); - } + binder.unbind(producerBinding); + binder.unbind(consumerBinding); } protected Collection getBindings(Binder testBinder) { if (testBinder instanceof AbstractTestBinder) { - return getBindingsFromBinder(((AbstractTestBinder) testBinder).getCoreBinder()); + return getBindingsFromBinder(((AbstractTestBinder) testBinder).getCoreBinder()); } return Collections.EMPTY_LIST; } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/AbstractTestBinder.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/AbstractTestBinder.java index f5caa5fd7..a4cc37e69 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/AbstractTestBinder.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/AbstractTestBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-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. @@ -22,19 +22,17 @@ import java.util.Set; import org.springframework.messaging.MessageChannel; - /** * Abstract class that adds test support for {@link Binder}. * * @author Ilayaperumal Gopinathan * @author Gary Russell + * @author Mark Fisher */ public abstract class AbstractTestBinder implements Binder { protected Set queues = new HashSet(); - protected Set topics = new HashSet(); - private C binder; public void setBinder(C binder) { @@ -48,45 +46,15 @@ public abstract class AbstractTestBinder } @Override - public void bindConsumer(String name, MessageChannel moduleInputChannel, Properties properties) { - binder.bindConsumer(name, moduleInputChannel, properties); + public Binding bindConsumer(String name, String group, MessageChannel moduleInputChannel, Properties properties) { queues.add(name); + return binder.bindConsumer(name, group, moduleInputChannel, properties); } @Override - public void bindPubSubConsumer(String name, MessageChannel inputChannel, String group, Properties properties) { - binder.bindPubSubConsumer(name, inputChannel, group, properties); - addTopic(name); - } - - @Override - public void bindProducer(String name, MessageChannel moduleOutputChannel, Properties properties) { - binder.bindProducer(name, moduleOutputChannel, properties); + public Binding bindProducer(String name, MessageChannel moduleOutputChannel, Properties properties) { queues.add(name); - } - - @Override - public void bindPubSubProducer(String name, MessageChannel outputChannel, Properties properties) { - binder.bindPubSubProducer(name, outputChannel, properties); - addTopic(name); - } - - @Override - public void bindRequestor(String name, MessageChannel requests, MessageChannel replies, - Properties properties) { - binder.bindRequestor(name, requests, replies, properties); - queues.add(name + ".requests"); - } - - @Override - public void bindReplier(String name, MessageChannel requests, MessageChannel replies, - Properties properties) { - binder.bindReplier(name, requests, replies, properties); - queues.add(name + ".requests"); - } - - private void addTopic(String topicName) { - topics.add("topic." + topicName); + return binder.bindProducer(name, moduleOutputChannel, properties); } public C getCoreBinder() { @@ -96,40 +64,8 @@ public abstract class AbstractTestBinder public abstract void cleanup(); @Override - public void unbindConsumers(String name) { - binder.unbindConsumers(name); - } - - @Override - public void unbindPubSubConsumers(String name, String group) { - binder.unbindPubSubConsumers(name, group); - } - - @Override - public void unbindProducers(String name) { - binder.unbindProducers(name); - } - - @Override - public void unbindConsumer(String name, MessageChannel channel) { - binder.unbindConsumer(name, channel); - } - - @Override - public void unbindProducer(String name, MessageChannel channel) { - binder.unbindProducer(name, channel); - } - - @Override - public MessageChannel bindDynamicProducer(String name, Properties properties) { - this.queues.add(name); - return this.binder.bindDynamicProducer(name, properties); - } - - @Override - public MessageChannel bindDynamicPubSubProducer(String name, Properties properties) { - this.topics.add(name); - return this.binder.bindDynamicPubSubProducer(name, properties); + public void unbind(Binding binding) { + binder.unbind(binding); } public C getBinder() { diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/BrokerBinderTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/BrokerBinderTests.java index 60e03ceab..59f9a926f 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/BrokerBinderTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/BrokerBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-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. @@ -16,91 +16,12 @@ package org.springframework.cloud.stream.binder; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; - -import java.util.Properties; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.Test; - -import org.springframework.integration.channel.DirectChannel; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessagingException; -import org.springframework.messaging.support.GenericMessage; - - /** * Tests for binders that use an external broker. * * @author Gary Russell */ -public abstract class BrokerBinderTests extends - AbstractBinderTests { - - @Test - public void testDirectBinding() throws Exception { - Binder binder = getBinder(); - Properties properties = new Properties(); - properties.setProperty(BinderPropertyKeys.DIRECT_BINDING_ALLOWED, "true"); - - DirectChannel moduleInputChannel = new DirectChannel(); - moduleInputChannel.setBeanName("direct.input"); - DirectChannel moduleOutputChannel = new DirectChannel(); - moduleOutputChannel.setBeanName("direct.output"); - binder.bindConsumer("direct.0", moduleInputChannel, null); - binder.bindProducer("direct.0", moduleOutputChannel, properties); - - final AtomicReference caller = new AtomicReference(); - final AtomicInteger count = new AtomicInteger(); - moduleInputChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - caller.set(Thread.currentThread()); - count.incrementAndGet(); - } - }); - - moduleOutputChannel.send(new GenericMessage("foo")); - moduleOutputChannel.send(new GenericMessage("foo")); - - assertNotNull(caller.get()); - assertSame(Thread.currentThread(), caller.get()); - assertEquals(2, count.get()); - assertNull(spyOn("direct.0").receive(true)); - - // Remove direct binding and bind the producer - binder.unbindConsumers("direct.0"); - binderBindUnbindLatency(); - - Spy spy = spyOn("direct.0"); - count.set(0); - moduleOutputChannel.send(new GenericMessage("bar")); - moduleOutputChannel.send(new GenericMessage("baz")); - Object bar = spy.receive(false); - assertEquals("bar", bar); - Object baz = spy.receive(false); - assertEquals("baz", baz); - assertEquals(0, count.get()); - - // Unbind producer from binder and bind directly again - caller.set(null); - binder.bindConsumer("direct.0", moduleInputChannel, null); - moduleOutputChannel.send(new GenericMessage("foo")); - moduleOutputChannel.send(new GenericMessage("foo")); - assertNotNull(caller.get()); - assertSame(Thread.currentThread(), caller.get()); - assertEquals(2, count.get()); - assertNull(spy.receive(true)); - - binder.unbindProducers("direct.0"); - binder.unbindConsumers("direct.0"); - } +public abstract class BrokerBinderTests extends AbstractBinderTests { /** * Create a new spy on the given 'queue'. This allows de-correlating the creation of @@ -109,6 +30,4 @@ public abstract class BrokerBinderTests extends */ public abstract Spy spyOn(final String name); - - } diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/PartitionCapableBinderTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/PartitionCapableBinderTests.java index d3e8262af..e3c2ad6fb 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/PartitionCapableBinderTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/main/java/org/springframework/cloud/stream/binder/PartitionCapableBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-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. @@ -43,11 +43,11 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.support.GenericMessage; - /** * Tests for binders that support partitioning. * * @author Gary Russell + * @author Mark Fisher */ abstract public class PartitionCapableBinderTests extends BrokerBinderTests { @@ -67,58 +67,59 @@ abstract public class PartitionCapableBinderTests extends BrokerBinderTests { + " does not support producer "), containsString("foo"), containsString("baz"), - containsString(" for badprops.0."))); + containsString(" for badprops.0"))); } properties.remove("baz"); try { - binder.bindConsumer("badprops.0", output, properties); + binder.bindConsumer("badprops.0", "test", output, properties); } catch (IllegalArgumentException e) { assertThat(e.getMessage(), equalTo(getClassUnderTestName() - + " does not support consumer property: foo for badprops.0.")); + + " does not support consumer property: foo for badprops.0.test.")); } } @Test public void testPartitionedModuleSpEL() throws Exception { Binder binder = getBinder(); - Properties properties = new Properties(); - properties.put("partitionKeyExpression", "payload"); - properties.put("partitionSelectorExpression", "hashCode()"); - properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "3"); - properties.put(BinderPropertyKeys.NEXT_MODULE_CONCURRENCY, "2"); + + Properties consumerProperties = new Properties(); + consumerProperties.put("concurrency", "2"); + consumerProperties.put("partitionIndex", "0"); + consumerProperties.put("count","3"); + QueueChannel input0 = new QueueChannel(); + input0.setBeanName("test.input0S"); + Binding input0Binding = binder.bindConsumer("part.0", "test", input0, consumerProperties); + consumerProperties.put("partitionIndex", "1"); + QueueChannel input1 = new QueueChannel(); + input1.setBeanName("test.input1S"); + Binding input1Binding = binder.bindConsumer("part.0", "test", input1, consumerProperties); + consumerProperties.put("partitionIndex", "2"); + QueueChannel input2 = new QueueChannel(); + input2.setBeanName("test.input2S"); + Binding input2Binding = binder.bindConsumer("part.0", "test", input2, consumerProperties); + + Properties producerProperties = new Properties(); + producerProperties.put("partitionKeyExpression", "payload"); + producerProperties.put("partitionSelectorExpression", "hashCode()"); + producerProperties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "3"); + producerProperties.put(BinderPropertyKeys.NEXT_MODULE_CONCURRENCY, "2"); DirectChannel output = new DirectChannel(); output.setBeanName("test.output"); - binder.bindProducer("part.0", output, properties); + Binding outputBinding = binder.bindProducer("part.0", output, producerProperties); @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); - assertEquals(1, bindings.size()); + List> bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + assertEquals(4, bindings.size()); try { - AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); - assertThat(getEndpointRouting(endpoint), containsString("part.0-' + headers['partition']")); + AbstractEndpoint endpoint = bindings.get(3).getEndpoint(); + assertThat(getEndpointRouting(endpoint), containsString( + getExpectedRoutingBaseDestination("part.0", "test") + "-' + 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"); - binder.bindConsumer("part.0", input0, properties); - properties.put("partitionIndex", "1"); - QueueChannel input1 = new QueueChannel(); - input1.setBeanName("test.input1S"); - binder.bindConsumer("part.0", input1, properties); - properties.put("partitionIndex", "2"); - QueueChannel input2 = new QueueChannel(); - input2.setBeanName("test.input2S"); - binder.bindConsumer("part.0", input2, properties); - Message message2 = MessageBuilder.withPayload(2) .setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, "foo") .setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, 42) @@ -148,17 +149,13 @@ abstract public class PartitionCapableBinderTests extends BrokerBinderTests { 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(), @@ -176,46 +173,48 @@ abstract public class PartitionCapableBinderTests extends BrokerBinderTests { containsOur3Messages); } - - binder.unbindConsumers("part.0"); - binder.unbindProducers("part.0"); + binder.unbind(input0Binding); + binder.unbind(input1Binding); + binder.unbind(input2Binding); + binder.unbind(outputBinding); } @Test public void testPartitionedModuleJava() throws Exception { Binder binder = getBinder(); - Properties properties = new Properties(); - properties.put("partitionKeyExtractorClass", "org.springframework.cloud.stream.binder.PartitionTestSupport"); - properties.put("partitionSelectorClass", "org.springframework.cloud.stream.binder.PartitionTestSupport"); - properties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "3"); - properties.put(BinderPropertyKeys.NEXT_MODULE_CONCURRENCY, "2"); - DirectChannel output = new DirectChannel(); - output.setBeanName("test.output"); - binder.bindProducer("partJ.0", output, properties); - @SuppressWarnings("unchecked") - List bindings = TestUtils.getPropertyValue(binder, "binder.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"); + Properties consumerProperties = new Properties(); + consumerProperties.put("concurrency", "2"); + consumerProperties.put("count","3"); + consumerProperties.put("partitionIndex", "0"); QueueChannel input0 = new QueueChannel(); input0.setBeanName("test.input0J"); - binder.bindConsumer("partJ.0", input0, properties); - properties.put("partitionIndex", "1"); + Binding input0Binding = binder.bindConsumer("partJ.0", "test", input0, consumerProperties); + consumerProperties.put("partitionIndex", "1"); QueueChannel input1 = new QueueChannel(); input1.setBeanName("test.input1J"); - binder.bindConsumer("partJ.0", input1, properties); - properties.put("partitionIndex", "2"); + Binding input1Binding = binder.bindConsumer("partJ.0", "test", input1, consumerProperties); + consumerProperties.put("partitionIndex", "2"); QueueChannel input2 = new QueueChannel(); input2.setBeanName("test.input2J"); - binder.bindConsumer("partJ.0", input2, properties); + Binding input2Binding = binder.bindConsumer("partJ.0", "test", input2, consumerProperties); + + Properties producerProperties = new Properties(); + producerProperties.put("partitionKeyExtractorClass", "org.springframework.cloud.stream.binder.PartitionTestSupport"); + producerProperties.put("partitionSelectorClass", "org.springframework.cloud.stream.binder.PartitionTestSupport"); + producerProperties.put(BinderPropertyKeys.NEXT_MODULE_COUNT, "3"); + producerProperties.put(BinderPropertyKeys.NEXT_MODULE_CONCURRENCY, "2"); + DirectChannel output = new DirectChannel(); + output.setBeanName("test.output"); + Binding outputBinding = binder.bindProducer("partJ.0", output, producerProperties); + @SuppressWarnings("unchecked") + List> bindings = TestUtils.getPropertyValue(binder, "binder.bindings", List.class); + assertEquals(4, bindings.size()); + if (usesExplicitRouting()) { + AbstractEndpoint endpoint = bindings.get(3).getEndpoint(); + assertThat(getEndpointRouting(endpoint), containsString( + getExpectedRoutingBaseDestination("partJ.0", "test") + "-' + headers['partition']")); + } output.send(new GenericMessage(2)); output.send(new GenericMessage(1)); @@ -242,8 +241,10 @@ abstract public class PartitionCapableBinderTests extends BrokerBinderTests { containsInAnyOrder(0, 1, 2)); } - binder.unbindConsumers("partJ.0"); - binder.unbindProducers("partJ.0"); + binder.unbind(input0Binding); + binder.unbind(input1Binding); + binder.unbind(input2Binding); + binder.unbind(outputBinding); } /** @@ -261,6 +262,14 @@ abstract public class PartitionCapableBinderTests extends BrokerBinderTests { throw new UnsupportedOperationException(); } + /** + * For implementations that rely on explicit routing, return the expected base destination + * (the part that precedes '-partition' within the expression). + */ + protected String getExpectedRoutingBaseDestination(String name, String group) { + throw new UnsupportedOperationException(); + } + /** * For implementations that rely on explicit routing, return the routing expression. */ diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/test/java/org/springframework/cloud/stream/binder/MessageChannelBinderSupportTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/test/java/org/springframework/cloud/stream/binder/MessageChannelBinderSupportTests.java index f633c9181..907483c0c 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/test/java/org/springframework/cloud/stream/binder/MessageChannelBinderSupportTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-test/src/test/java/org/springframework/cloud/stream/binder/MessageChannelBinderSupportTests.java @@ -269,31 +269,13 @@ public class MessageChannelBinderSupportTests { public class TestMessageChannelBinder extends MessageChannelBinderSupport { @Override - public void bindConsumer(String name, MessageChannel channel, Properties properties) { + protected Binding doBindConsumer(String name, String group, MessageChannel channel, Properties properties) { + return null; } @Override - public void bindPubSubConsumer(String name, MessageChannel moduleInputChannel, String group, - Properties properties) { - } - - @Override - public void bindPubSubProducer(String name, MessageChannel moduleOutputChannel, - Properties properties) { - } - - @Override - public void bindProducer(String name, MessageChannel channel, Properties properties) { - } - - @Override - public void bindRequestor(String name, MessageChannel requests, MessageChannel replies, - Properties properties) { - } - - @Override - public void bindReplier(String name, MessageChannel requests, MessageChannel replies, - Properties properties) { + public Binding bindProducer(String name, MessageChannel channel, Properties properties) { + return null; } } diff --git a/spring-cloud-stream-samples/multibinder-differentsystems/src/test/java/multibinder/TwoKafkaBindersApplicationTest.java b/spring-cloud-stream-samples/multibinder-differentsystems/src/test/java/multibinder/TwoKafkaBindersApplicationTest.java index 62dd8dc0b..918e97f87 100644 --- a/spring-cloud-stream-samples/multibinder-differentsystems/src/test/java/multibinder/TwoKafkaBindersApplicationTest.java +++ b/spring-cloud-stream-samples/multibinder-differentsystems/src/test/java/multibinder/TwoKafkaBindersApplicationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -76,8 +76,8 @@ public class TwoKafkaBindersApplicationTest { binderFactory.getBinder("kafka1").bindProducer("dataIn", dataProducer, null); QueueChannel dataConsumer = new QueueChannel(); - binderFactory.getBinder("kafka2").bindPubSubConsumer("dataOut", dataConsumer, - UUID.randomUUID().toString(), null); + binderFactory.getBinder("kafka2").bindConsumer("dataOut", UUID.randomUUID().toString(), + dataConsumer, null); String testPayload = "testFoo" + UUID.randomUUID().toString(); dataProducer.send(MessageBuilder.withPayload(testPayload).build()); diff --git a/spring-cloud-stream-samples/multibinder/src/test/java/multibinder/RabbitAndRedisBinderApplicationTests.java b/spring-cloud-stream-samples/multibinder/src/test/java/multibinder/RabbitAndRedisBinderApplicationTests.java index 6d8981831..93b70f19d 100644 --- a/spring-cloud-stream-samples/multibinder/src/test/java/multibinder/RabbitAndRedisBinderApplicationTests.java +++ b/spring-cloud-stream-samples/multibinder/src/test/java/multibinder/RabbitAndRedisBinderApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -60,10 +60,13 @@ public class RabbitAndRedisBinderApplicationTests { @Autowired private BinderFactory binderFactory; + private final String randomGroup = UUID.randomUUID().toString(); + @After public void cleanUp() { RabbitAdmin admin = new RabbitAdmin(rabbitTestSupport.getResource()); - admin.deleteQueue("binder.dataOut"); + admin.deleteQueue("binder.dataOut.default"); + admin.deleteQueue("binder.dataOut." + this.randomGroup); admin.deleteExchange("binder.dataOut"); } @@ -77,10 +80,10 @@ public class RabbitAndRedisBinderApplicationTests { binderFactory.getBinder("redis").bindProducer("dataIn", dataProducer, null); QueueChannel dataConsumer = new QueueChannel(); - binderFactory.getBinder("rabbit").bindPubSubConsumer("dataOut", dataConsumer, - UUID.randomUUID().toString(), null); + binderFactory.getBinder("rabbit").bindConsumer("dataOut", this.randomGroup, + dataConsumer, null); - String testPayload = "testFoo" + UUID.randomUUID().toString(); + String testPayload = "testFoo" + this.randomGroup; dataProducer.send(MessageBuilder.withPayload(testPayload).build()); Message receive = dataConsumer.receive(2000); diff --git a/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/test/binder/TestSupportBinder.java b/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/test/binder/TestSupportBinder.java index 79cf12dc2..921814416 100644 --- a/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/test/binder/TestSupportBinder.java +++ b/spring-cloud-stream-test-support/src/main/java/org/springframework/cloud/stream/test/binder/TestSupportBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -23,6 +23,7 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingDeque; import org.springframework.cloud.stream.binder.Binder; +import org.springframework.cloud.stream.binder.Binding; import org.springframework.cloud.stream.test.matcher.MessageQueueMatcher; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -39,6 +40,7 @@ import org.springframework.util.Assert; * * @author Eric Bottard * @author Gary Russell + * @author Mark Fisher * @see MessageQueueMatcher */ public class TestSupportBinder implements Binder { @@ -47,78 +49,29 @@ public class TestSupportBinder implements Binder { @Override - public void bindConsumer(String name, MessageChannel inboundBindTarget, Properties properties) { - } - - @Override - public void bindPubSubConsumer(String name, MessageChannel inboundBindTarget, String group, Properties properties) { - + public Binding bindConsumer(String name, String group, MessageChannel inboundBindTarget, Properties properties) { + return null; } /** * Registers a single subscriber to the channel, that enqueues messages for later retrieval and assertion in tests. */ @Override - @SuppressWarnings("unchecked") - public void bindProducer(String name, MessageChannel outboundBindTarget, Properties properties) { - final BlockingQueue queue = messageCollector.register(outboundBindTarget); + public Binding bindProducer(String name, MessageChannel outboundBindTarget, Properties properties) { + final BlockingQueue> queue = messageCollector.register(outboundBindTarget); ((SubscribableChannel)outboundBindTarget).subscribe(new MessageHandler() { @Override public void handleMessage(Message message) throws MessagingException { queue.add(message); } }); - - } - - @Override - public void unbindProducer(String name, MessageChannel channel) { - messageCollector.unregister(channel); - } - - @Override - public void bindPubSubProducer(String name, MessageChannel outboundBindTarget, Properties properties) { - - } - - @Override - public void unbindConsumers(String name) { - - } - - @Override - public void unbindPubSubConsumers(String name, String group) { - - } - - @Override - public void unbindProducers(String name) { - - } - - @Override - public void unbindConsumer(String name, MessageChannel inboundBindTarget) { - - } - - @Override - public void bindRequestor(String name, MessageChannel requests, MessageChannel replies, Properties properties) { - - } - - @Override - public void bindReplier(String name, MessageChannel requests, MessageChannel replies, Properties properties) { - - } - - @Override - public MessageChannel bindDynamicProducer(String name, Properties properties) { return null; } @Override - public MessageChannel bindDynamicPubSubProducer(String name, Properties properties) { - return null; + public void unbind(Binding binding) { + if (Binding.Type.producer.equals(binding.getType())) + messageCollector.unregister(binding.getTarget()); } public MessageCollector messageCollector() { @@ -134,7 +87,7 @@ public class TestSupportBinder implements Binder { private final Map>> results = new HashMap<>(); - private BlockingQueue register(MessageChannel channel) { + private BlockingQueue> register(MessageChannel channel) { LinkedBlockingDeque> result = new LinkedBlockingDeque<>(); Assert.isTrue(!results.containsKey(channel), "Channel [" + channel + "] was already bound"); results.put(channel, result); diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/AbstractBindingPropertiesAccessor.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/AbstractBindingPropertiesAccessor.java index c0172bb06..55ad48f6f 100644 --- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/AbstractBindingPropertiesAccessor.java +++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/AbstractBindingPropertiesAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-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. @@ -288,15 +288,6 @@ public abstract class AbstractBindingPropertiesAccessor { return getProperty(BinderPropertyKeys.PARTITION_INDEX, -1); } - // Direct Binding - - /** - * If true, the binder can attempt a direct binding. - */ - public boolean isDirectBindingAllowed() { - return getProperty(BinderPropertyKeys.DIRECT_BINDING_ALLOWED, false); - } - // Batching /** diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binder.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binder.java index 48f8de10b..fdd4b66dc 100644 --- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binder.java +++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binder.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-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. @@ -32,109 +32,27 @@ import java.util.Properties; public interface Binder { /** - * Bind a message consumer on a p2p channel + * Bind a message consumer on a channel * @param name the logical identity of the message source - * @param inboundBindTarget the module interface to be bound as a point to point consumer - * @param properties arbitrary String key/value pairs that will be used in the binding - */ - void bindConsumer(String name, T inboundBindTarget, Properties properties); - - /** - * Bind a message consumer on a pub/sub channel - * @param name the logical identity of the message source - * @param inboundBindTarget the module interface to be bound as a pub/sub consumer * @param group the consumer group to which this consumer belongs - subscriptions are shared among consumers - * in the same group + * in the same group (if null or empty String, the "default" group will be used) + * @param inboundBindTarget the module interface to be bound as a consumer * @param properties arbitrary String key/value pairs that will be used in the binding */ - void bindPubSubConsumer(final String name, T inboundBindTarget, String group, Properties properties); + Binding bindConsumer(String name, String group, T inboundBindTarget, Properties properties); /** - * Bind a message producer on a p2p channel. + * Bind a message producer on a channel. * @param name the logical identity of the message target * @param outboundBindTarget the module interface bound as a producer * @param properties arbitrary String key/value pairs that will be used in the binding */ - void bindProducer(String name, T outboundBindTarget, Properties properties); - + Binding bindProducer(String name, T outboundBindTarget, Properties properties); /** - * Bind a message producer on a pub/sub channel. - * @param name the logical identity of the message target - * @param outboundBindTarget the module interface bound as a producer - * @param properties arbitrary String key/value pairs that will be used in the binding + * Unbind the target component represented by the provided Binding and stop any active components. + * @param binding the Binding instance to unbind */ - void bindPubSubProducer(final String name, T outboundBindTarget, Properties properties); - - /** - * Unbind inbound module components and stop any active components that use the channel. - * @param name the channel name - */ - void unbindConsumers(String name); - - /** - * Unbind inbound module components and stop any active components that use the channel - * with the supplied consumer group. - * @param name the channel name - * @param group the consumer group - */ - void unbindPubSubConsumers(String name, String group); - - /** - * Unbind outbound module components and stop any active components that use the channel. - * @param name the channel name - */ - void unbindProducers(String name); - - /** - * Unbind a specific p2p or pub/sub message consumer - * @param name The logical identify of a message source - * @param inboundBindTarget The module interface bound as a consumer - */ - void unbindConsumer(String name, T inboundBindTarget); - - /** - * Unbind a specific p2p or pub/sub message producer - * @param name the logical identity of the message target - * @param outboundBindTarget the channel bound as a producer - */ - void unbindProducer(String name, T outboundBindTarget); - - /** - * Bind a producer that expects async replies. To unbind, invoke unbindProducer() and unbindConsumer(). - * @param name The name of the requestor. - * @param requests The interface used to send requests. - * @param replies The interface used to receive replies. - * @param properties arbitrary String key/value pairs that will be used in the binding. - */ - void bindRequestor(String name, T requests, T replies, Properties properties); - - /** - * Bind a consumer that handles requests from a requestor and asynchronously sends replies. To unbind, invoke - * unbindProducer() and unbindConsumer(). - * @param name The name of the requestor for which this replier will handle requests. - * @param requests The interface used to send requests. - * @param replies The interface used to receive replies. - * @param properties arbitrary String key/value pairs that will be used in the binding. - */ - void bindReplier(String name, T requests, T replies, Properties properties); - - /** - * Create an object and bind a producer dynamically, creating the infrastructure - * required by the binder technology. - * @param name The name of the "queue:" channel. - * @param properties arbitrary String key/value pairs that will be used in the binding. - * @return The bound object. - */ - T bindDynamicProducer(String name, Properties properties); - - /** - * Create an object and bind a producer dynamically, creating the infrastructure - * required by the binder technology to broadcast messages to consumers. - * @param name The name of the "topic:" channel. - * @param properties arbitrary String key/value pairs that will be used in the binding. - * @return The bound Object. - */ - T bindDynamicPubSubProducer(String name, Properties properties); + void unbind(Binding binding); } diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderPropertyKeys.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderPropertyKeys.java index 10fbf1b61..9cb753b98 100644 --- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderPropertyKeys.java +++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderPropertyKeys.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-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. @@ -16,7 +16,6 @@ package org.springframework.cloud.stream.binder; - /** * Common binder properties. * @@ -92,11 +91,6 @@ public abstract class BinderPropertyKeys { */ public static final String PARTITION_SELECTOR_EXPRESSION = "partitionSelectorExpression"; - /** - * If true, the binder will attempt to create a direct binding between the producer and consumer. - */ - public static final String DIRECT_BINDING_ALLOWED = "directBindingAllowed"; - /** * True if message batching is enabled. */ diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderUtils.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderUtils.java deleted file mode 100644 index 662781120..000000000 --- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/BinderUtils.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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.cloud.stream.binder; - -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * Binder utilities. - * - * @author Gary Russell - */ -public class BinderUtils { - - /** - * The delimiter between a group and index when constructing a binder consumer/producer. - */ - public static final String GROUP_INDEX_DELIMITER = "."; - - /** - * The prefix for the consumer/producer when creating a topic. - */ - public static final String TOPIC_CHANNEL_PREFIX = "topic:"; - - /** - * Determine whether the provided channel name represents a pub/sub channel (i.e. topic or tap). - * @param channelName name of the channel to check - * @return true if pub/sub. - */ - public static boolean isChannelPubSub(String channelName) { - Assert.isTrue(StringUtils.hasText(channelName), "Channel name should not be empty/null."); - return channelName.startsWith(TOPIC_CHANNEL_PREFIX); - } - - /** - * Construct a name comprised of the group and name. - * @param name the name. - * @param group the group. - * @return the constructed name. - */ - public static String groupedName(String name, String group) { - return group == null ? name : group + BinderUtils.GROUP_INDEX_DELIMITER + name; - } - -} diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binding.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binding.java index aa653f983..f8e9884fa 100644 --- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binding.java +++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/Binding.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-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. @@ -18,76 +18,74 @@ package org.springframework.cloud.stream.binder; import org.springframework.context.Lifecycle; import org.springframework.integration.endpoint.AbstractEndpoint; -import org.springframework.messaging.MessageChannel; import org.springframework.util.Assert; /** - * Represents a binding between a module's channel and an adapter endpoint that connects to the Binder. The binding - * could be for a consumer or a producer. A consumer binding represents a connection from an adapter on the binder to a - * module's input channel. A producer binding represents a connection from a module's output channel to an adapter on - * the binder. + * Represents a binding between a channel and an adapter endpoint that connects via a Binder. The binding + * could be for a consumer or a producer. A consumer binding represents a connection from an adapter to an + * input channel. A producer binding represents a connection from an output channel to an adapter. * * @author Jennifer Hickey * @author Mark Fisher * @author Gary Russell */ -public class Binding implements Lifecycle { +public class Binding implements Lifecycle { - public static final String PRODUCER = "producer"; - - public static final String CONSUMER = "consumer"; - - public static final String DIRECT = "direct"; + public static enum Type { + producer, consumer + } private final String name; - private final MessageChannel channel; + private final String group; + + private final T target; private final AbstractEndpoint endpoint; - private final String type; + private final Type type; private final AbstractBindingPropertiesAccessor properties; - private Binding(String name, MessageChannel channel, AbstractEndpoint endpoint, String type, + private Binding(String name, String group, T target, AbstractEndpoint endpoint, Type type, AbstractBindingPropertiesAccessor properties) { - Assert.notNull(channel, "channel must not be null"); + Assert.notNull(target, "target must not be null"); Assert.notNull(endpoint, "endpoint must not be null"); this.name = name; - this.channel = channel; + this.group = group; + this.target = target; this.endpoint = endpoint; this.type = type; this.properties = properties; } - public static Binding forConsumer(String name, AbstractEndpoint adapterFromBinder, MessageChannel moduleInputChannel, + public static Binding forConsumer(String name, String group, AbstractEndpoint adapterFromBinder, T inputTarget, AbstractBindingPropertiesAccessor properties) { - return new Binding(name, moduleInputChannel, adapterFromBinder, CONSUMER, properties); + return new Binding(name, group, inputTarget, adapterFromBinder, Type.consumer, properties); } - public static Binding forProducer(String name, MessageChannel moduleOutputChannel, AbstractEndpoint adapterToBinder, + public static Binding forProducer(String name, T outputTarget, AbstractEndpoint adapterToBinder, AbstractBindingPropertiesAccessor properties) { - return new Binding(name, moduleOutputChannel, adapterToBinder, PRODUCER, properties); - } - - public static Binding forDirectProducer(String name, MessageChannel moduleOutputChannel, - AbstractEndpoint adapter, AbstractBindingPropertiesAccessor properties) { - return new Binding(name, moduleOutputChannel, adapter, DIRECT, properties); + return new Binding(name, null, outputTarget, adapterToBinder, Type.producer, properties); } public String getName() { return name; } - public MessageChannel getChannel() { - return channel; + public String getGroup() { + return group; + } + + public T getTarget() { + return target; } public AbstractEndpoint getEndpoint() { return endpoint; } - public String getType() { + public Type getType() { return type; } @@ -112,8 +110,60 @@ public class Binding implements Lifecycle { @Override public String toString() { - return type + " Binding [name=" + name + ", channel=" + channel + ", endpoint=" + endpoint.getComponentName() + return type + " Binding [name=" + name + ", target=" + target + ", endpoint=" + endpoint.getComponentName() + "]"; } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((endpoint == null) ? 0 : endpoint.hashCode()); + result = prime * result + ((group == null) ? 0 : group.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((properties == null) ? 0 : properties.hashCode()); + result = prime * result + ((target == null) ? 0 : target.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Binding other = (Binding) obj; + if (endpoint == null) { + if (other.endpoint != null) + return false; + } else if (!endpoint.equals(other.endpoint)) + return false; + if (group == null) { + if (other.group != null) + return false; + } else if (!group.equals(other.group)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (properties == null) { + if (other.properties != null) + return false; + } else if (!properties.equals(other.properties)) + return false; + if (target == null) { + if (other.target != null) + return false; + } else if (!target.equals(other.target)) + return false; + if (type != other.type) + return false; + return true; + } + } diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/MessageChannelBinderSupport.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/MessageChannelBinderSupport.java index 08027e54b..b3b040da2 100644 --- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/MessageChannelBinderSupport.java +++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/MessageChannelBinderSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-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. @@ -29,7 +29,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -56,14 +55,12 @@ import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.codec.Codec; -import org.springframework.integration.endpoint.EventDrivenConsumer; import org.springframework.integration.expression.ExpressionUtils; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.MessagingException; -import org.springframework.messaging.SubscribableChannel; import org.springframework.retry.backoff.ExponentialBackOffPolicy; import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; @@ -78,18 +75,23 @@ import org.springframework.util.StringUtils; * @author David Turanski * @author Gary Russell * @author Ilayaperumal Gopinathan + * @author Mark Fisher */ public abstract class MessageChannelBinderSupport implements Binder, ApplicationContextAware, InitializingBean { - protected static final String P2P_NAMED_CHANNEL_TYPE_PREFIX = "queue:"; - - protected static final String PUBSUB_NAMED_CHANNEL_TYPE_PREFIX = "topic:"; - - protected static final String JOB_CHANNEL_TYPE_PREFIX = "job:"; - protected static final String PARTITION_HEADER = "partition"; + /** + * Default group name (used if null or empty String is provided). + */ + protected static final String DEFAULT_CONSUMER_GROUP = "default"; + + /** + * The delimiter between a group and index when constructing a binder consumer/producer. + */ + private static final String GROUP_INDEX_DELIMITER = "."; + protected final Logger logger = LoggerFactory.getLogger(getClass()); private volatile AbstractApplicationContext applicationContext; @@ -98,8 +100,6 @@ public abstract class MessageChannelBinderSupport private final StringConvertingContentTypeResolver contentTypeResolver = new StringConvertingContentTypeResolver(); - private final ThreadLocal revertingDirectBinding = new ThreadLocal(); - protected static final List MEDIATYPES_MEDIATYPE_ALL = Collections.singletonList(ALL); private static final int DEFAULT_BACKOFF_INITIAL_INTERVAL = 1000; @@ -161,7 +161,7 @@ public abstract class MessageChannelBinderSupport BinderPropertyKeys.BATCH_BUFFER_LIMIT, })); - private final List bindings = Collections.synchronizedList(new ArrayList()); + private final List> bindings = Collections.synchronizedList(new ArrayList>()); private final IdGenerator idGenerator = new AlternativeJdkIdGenerator(); @@ -206,7 +206,7 @@ public abstract class MessageChannelBinderSupport protected volatile boolean defaultCompress = false; - protected volatile boolean defaultDurableSubscription = false; + protected volatile boolean defaultDurableSubscription = true; // Payload type cache private volatile Map> payloadTypeCache = new ConcurrentHashMap<>(); @@ -220,23 +220,6 @@ public abstract class MessageChannelBinderSupport return prefix + name; } - /** - * For binder implementations that include a pub/sub component in identifiers, construct the name. - * @param name the name. - */ - public static String applyPubSub(String name) { - return "topic." + name; - } - - /** - * Build the requests entity name. - * @param name the name. - * @return the request entity name. - */ - public static String applyRequests(String name) { - return name + ".requests"; - } - /** * For binder implementations that support dead lettering, construct the name of the dead letter entity for the * underlying pipe name. @@ -383,17 +366,14 @@ public abstract class MessageChannelBinderSupport } } - /** - * Dynamically create a producer for the named channel. - * @param name The name. - * @param properties The properties. - * @return The channel. - */ @Override - public MessageChannel bindDynamicProducer(String name, Properties properties) { - return doBindDynamicProducer(name, name, properties); + public final Binding bindConsumer(String name, String group, MessageChannel inputChannel, Properties properties) { + group = (StringUtils.hasText(group)) ? group : DEFAULT_CONSUMER_GROUP; + return doBindConsumer(name, group, inputChannel, properties); } + protected abstract Binding doBindConsumer(String name, String group, MessageChannel inputChannel, Properties properties); + /** * Create a producer for the named channel and bind it to the binder. Synchronized to avoid creating multiple * instances. @@ -419,44 +399,6 @@ public abstract class MessageChannelBinderSupport return channel; } - /** - * Dynamically create a producer for the named channel. Note: even though it's pub/sub, we still use a direct - * channel. It will be bridged to a pub/sub channel in the local binder and bound to an appropriate element for other - * binders. - * @param name The name. - * @param properties The properties. - * @return The channel. - */ - @Override - public MessageChannel bindDynamicPubSubProducer(String name, Properties properties) { - return doBindDynamicPubSubProducer(name, name, properties); - } - - /** - * Create a producer for the named channel and bind it to the binder. Synchronized to avoid creating multiple - * instances. - * @param name The name. - * @param channelName The name of the channel to be created, and registered as bean. - * @param properties The properties. - * @return The channel. - */ - protected synchronized MessageChannel doBindDynamicPubSubProducer(String name, String channelName, - Properties properties) { - MessageChannel channel = this.directChannelProvider.lookupSharedChannel(channelName); - if (channel == null) { - try { - channel = this.directChannelProvider.createAndRegisterChannel(channelName); - bindPubSubProducer(name, channel, properties); - } - catch (RuntimeException e) { - destroyCreatedChannel(channelName, channel); - throw new BinderException( - "Failed to bind dynamic channel '" + name + "' with properties " + properties, e); - } - } - return channel; - } - private void destroyCreatedChannel(String name, MessageChannel channel) { BeanFactory beanFactory = this.applicationContext.getBeanFactory(); if (beanFactory.containsBean(name)) { @@ -467,82 +409,17 @@ public abstract class MessageChannelBinderSupport } @Override - public void unbindConsumers(String name) { - deleteBindings("inbound." + name); - } - - @Override - public void unbindPubSubConsumers(String name, String group) { - unbindConsumers(BinderUtils.groupedName(name, group)); - } - - @Override - public void unbindProducers(String name) { - deleteBindings("outbound." + name); - } - - @Override - public void unbindConsumer(String name, MessageChannel channel) { - deleteBinding("inbound." + name, channel); - } - - @Override - public void unbindProducer(String name, MessageChannel channel) { - deleteBinding("outbound." + name, channel); - } - - protected void addBinding(Binding binding) { - this.bindings.add(binding); - } - - protected void deleteBindings(String name) { - Assert.hasText(name, "a valid name is required to remove bindings"); - List bindingsToRemove = new ArrayList(); - synchronized (this.bindings) { - Iterator iterator = this.bindings.iterator(); - while (iterator.hasNext()) { - Binding binding = iterator.next(); - if (binding.getEndpoint().getComponentName().equals(name)) { - bindingsToRemove.add(binding); - } - } - for (Binding binding : bindingsToRemove) { - doDeleteBinding(binding); - } - } - } - - protected void deleteBinding(String name, MessageChannel channel) { - Assert.hasText(name, "a valid name is required to remove a binding"); - Assert.notNull(channel, "a valid channel is required to remove a binding"); - Binding bindingToRemove = null; - synchronized (this.bindings) { - Iterator iterator = this.bindings.iterator(); - while (iterator.hasNext()) { - Binding binding = iterator.next(); - if (binding.getChannel().equals(channel) && - binding.getEndpoint().getComponentName().equals(name)) { - bindingToRemove = binding; - break; - } - } - if (bindingToRemove != null) { - doDeleteBinding(bindingToRemove); - } - } - - } - - private void doDeleteBinding(Binding binding) { - if (Binding.CONSUMER.equals(binding.getType())) { - /* - * Revert the direct binding before stopping the consumer; the module - * outputChannel will temporarily have 2 subscribers. - */ - revertDirectBindingIfNecessary(binding); - } + public void unbind(Binding binding) { binding.stop(); this.bindings.remove(binding); + afterUnbind(binding); + } + + protected void afterUnbind(Binding binding) { + } + + protected void addBinding(Binding binding) { + this.bindings.add(binding); } protected void stopBindings() { @@ -558,6 +435,19 @@ public abstract class MessageChannelBinderSupport } } + /** + * Construct a name comprised of the name and group. + * @param name the name. + * @param group the group. + * @return the constructed name. + */ + protected final String groupedName(String name, String group) { + if (!StringUtils.hasText(group)) { + group = "default"; + } + return name + GROUP_INDEX_DELIMITER + group; + } + protected final MessageValues serializePayloadIfNecessary(Message message) { Object originalPayload = message.getPayload(); Object originalContentType = message.getHeaders().get(MessageHeaders.CONTENT_TYPE); @@ -821,125 +711,6 @@ public abstract class MessageChannelBinderSupport } } - protected boolean isNamedChannel(String name) { - return name.startsWith(PUBSUB_NAMED_CHANNEL_TYPE_PREFIX) || name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX) - || name.startsWith(JOB_CHANNEL_TYPE_PREFIX); - } - - /** - * Attempt to create a direct binding (avoiding the broker) if the consumer is local. Named channel producers are not - * bound directly. - * @param name The name. - * @param moduleOutputChannel The channel to bind. - * @param properties The producer properties. - * @return true if the producer is bound. - */ - protected boolean bindNewProducerDirectlyIfPossible(String name, SubscribableChannel moduleOutputChannel, - AbstractBindingPropertiesAccessor properties) { - if (!properties.isDirectBindingAllowed()) { - return false; - } - else if (isNamedChannel(name)) { - return false; - } - else if (this.revertingDirectBinding.get() != null) { - // we're in the process of unbinding a direct binding - this.revertingDirectBinding.remove(); - return false; - } - else { - Binding consumerBinding = null; - synchronized (this.bindings) { - for (Binding binding : this.bindings) { - if (binding.getName().equals(name) && Binding.CONSUMER.equals(binding.getType())) { - consumerBinding = binding; - break; - } - } - } - if (consumerBinding == null) { - return false; - } - else { - bindProducerDirectly(name, moduleOutputChannel, consumerBinding.getChannel(), properties); - return true; - } - } - } - - private void bindProducerDirectly(String name, SubscribableChannel producerChannel, - MessageChannel consumerChannel, AbstractBindingPropertiesAccessor properties) { - DirectHandler handler = new DirectHandler(consumerChannel); - EventDrivenConsumer consumer = new EventDrivenConsumer(producerChannel, handler); - consumer.setBeanFactory(getBeanFactory()); - consumer.setBeanName("outbound." + name); - consumer.afterPropertiesSet(); - Binding binding = Binding.forDirectProducer(name, producerChannel, consumer, properties); - addBinding(binding); - binding.start(); - if (this.logger.isInfoEnabled()) { - this.logger.info("Producer bound directly: " + binding); - } - } - - /** - * Attempt to bind a producer directly (avoiding the broker) if there is already a local producer. PubSub producers - * cannot be bound directly. Create the direct binding, then unbind the existing producer. - * @param name The name. - * @param consumerChannel The channel to bind the producer to. - */ - protected void bindExistingProducerDirectlyIfPossible(String name, MessageChannel consumerChannel) { - if (!isNamedChannel(name)) { - Binding producerBinding = null; - synchronized (this.bindings) { - for (Binding binding : this.bindings) { - if (binding.getName().equals(name) && Binding.PRODUCER.equals(binding.getType())) { - producerBinding = binding; - break; - } - } - if (producerBinding != null && producerBinding.getChannel() instanceof SubscribableChannel) { - AbstractBindingPropertiesAccessor properties = producerBinding.getPropertiesAccessor(); - if (properties.isDirectBindingAllowed()) { - bindProducerDirectly(name, (SubscribableChannel) producerBinding.getChannel(), consumerChannel, - properties); - producerBinding.stop(); - this.bindings.remove(producerBinding); - } - } - } - } - } - - private void revertDirectBindingIfNecessary(Binding binding) { - try { - synchronized (this.bindings) { // Not necessary, called while synchronized, but just in case... - Binding directBinding = null; - Iterator iterator = this.bindings.iterator(); - while (iterator.hasNext()) { - Binding producer = iterator.next(); - if (Binding.DIRECT.equals(producer.getType()) && binding.getName().equals(producer.getName())) { - this.revertingDirectBinding.set(Boolean.TRUE); - bindProducer(producer.getName(), producer.getChannel(), - producer.getPropertiesAccessor().getProperties()); - directBinding = producer; - break; - } - } - if (directBinding != null) { - directBinding.stop(); - this.bindings.remove(directBinding); - if (this.logger.isInfoEnabled()) { - this.logger.info("direct binding reverted: " + directBinding); - } - } - } - } - catch (Exception e) { - this.logger.error("Could not revert direct binding: " + binding, e); - } - } - /** * Default partition strategy; only works on keys with "real" hash codes, such as String. Caller now always applies * modulo so no need to do so here. diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BinderAwareChannelResolver.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BinderAwareChannelResolver.java index 02c5dfba0..26bc24c15 100644 --- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BinderAwareChannelResolver.java +++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/BinderAwareChannelResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-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. @@ -18,15 +18,21 @@ package org.springframework.cloud.stream.binding; import java.util.Properties; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.cloud.stream.binder.Binder; import org.springframework.cloud.stream.binder.BinderFactory; +import org.springframework.integration.channel.DirectChannel; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.core.BeanFactoryMessageChannelDestinationResolver; import org.springframework.messaging.core.DestinationResolutionException; /** - * A {@link org.springframework.messaging.core.DestinationResolver} implementation that first checks for any channel - * whose name begins with a colon in the {@link Binder}. + * A {@link org.springframework.messaging.core.DestinationResolver} implementation that + * resolves the channel from the bean factory and, if not present, creates a new channel + * and adds it to the factory after binding it to the binder. The binder is optionally + * determined with a prefix preceding a colon. * @author Mark Fisher * @author Gary Russell */ @@ -36,11 +42,21 @@ public class BinderAwareChannelResolver extends BeanFactoryMessageChannelDestina private final Properties producerProperties; + private DefaultListableBeanFactory beanFactory; + public BinderAwareChannelResolver(BinderFactory binderFactory, Properties producerProperties) { this.binderFactory = binderFactory; this.producerProperties = producerProperties; } + @Override + public void setBeanFactory(BeanFactory beanFactory) { + super.setBeanFactory(beanFactory); + if (beanFactory instanceof ConfigurableBeanFactory) { + this.beanFactory = (DefaultListableBeanFactory) beanFactory; + } + } + @Override public MessageChannel resolveDestination(String name) { MessageChannel channel = null; @@ -49,38 +65,32 @@ public class BinderAwareChannelResolver extends BeanFactoryMessageChannelDestina } catch (DestinationResolutionException e) { } - if (name.contains(":")) { - if (binderFactory != null) { - String[] tokens = name.split(":", 2); + synchronized (this) { + try { + return super.resolveDestination(name); + } + catch (DestinationResolutionException e) { + } + if (this.beanFactory != null && this.binderFactory != null) { + channel = new DirectChannel(); + this.beanFactory.registerSingleton(name, channel); + channel = (MessageChannel) this.beanFactory.initializeBean(channel, name); String transport = null; - String type; - if (tokens.length == 2) { - type = tokens[0]; - } - else if (tokens.length == 3) { - transport = tokens[0]; - type = tokens[1]; - } - else { - throw new IllegalArgumentException("Unrecognized channel naming scheme: " + name + " , should be" + - " [:]:"); + if (name.contains(":")) { + String[] tokens = name.split(":", 2); + if (tokens.length == 2) { + transport = tokens[0]; + } + else if (tokens.length != 1) { + throw new IllegalArgumentException("Unrecognized channel naming scheme: " + name + " , should be" + + " [:]"); + } } Binder binder = binderFactory.getBinder(transport); - if ("queue".equals(type)) { - channel = binder.bindDynamicProducer(name, this.producerProperties); - } - else if ("topic".equals(type)) { - channel = binder.bindDynamicPubSubProducer(name, this.producerProperties); - } - else { - throw new IllegalArgumentException("unrecognized channel type: " + type); - } + binder.bindProducer(name, channel, this.producerProperties); } + return channel; } - if (channel == null) { - channel = super.resolveDestination(name); - } - return channel; } } diff --git a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/ChannelBindingService.java b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/ChannelBindingService.java index 0192df052..bb24a6a57 100644 --- a/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/ChannelBindingService.java +++ b/spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binding/ChannelBindingService.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -16,14 +16,15 @@ package org.springframework.cloud.stream.binding; +import java.util.HashMap; +import java.util.Map; + import org.springframework.cloud.stream.binder.Binder; import org.springframework.cloud.stream.binder.BinderFactory; -import org.springframework.cloud.stream.binder.BinderUtils; +import org.springframework.cloud.stream.binder.Binding; import org.springframework.cloud.stream.config.BindingProperties; import org.springframework.cloud.stream.config.ChannelBindingServiceProperties; import org.springframework.messaging.MessageChannel; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * Handles the operations related to channel binding including binding of input/output channels by delegating @@ -41,57 +42,42 @@ public class ChannelBindingService { private final ChannelBindingServiceProperties channelBindingServiceProperties; + private final Map> producerBindings = new HashMap<>(); + + private final Map> consumerBindings = new HashMap<>(); + public ChannelBindingService(ChannelBindingServiceProperties channelBindingServiceProperties, BinderFactory binderFactory) { this.channelBindingServiceProperties = channelBindingServiceProperties; this.binderFactory = binderFactory; } - public void bindConsumer(MessageChannel inputChannel, String inputChannelName) { + public Binding bindConsumer(MessageChannel inputChannel, String inputChannelName) { String channelBindingTarget = this.channelBindingServiceProperties.getBindingDestination(inputChannelName); Binder binder = getBinderForChannel(inputChannelName); - if (BinderUtils.isChannelPubSub(channelBindingTarget)) { - binder.bindPubSubConsumer(removePrefix(channelBindingTarget), - inputChannel, consumerGroup(inputChannelName), - this.channelBindingServiceProperties.getConsumerProperties(inputChannelName)); - } - else { - binder.bindConsumer(channelBindingTarget, inputChannel, - this.channelBindingServiceProperties.getConsumerProperties(inputChannelName)); - } + Binding binding = binder.bindConsumer(channelBindingTarget, consumerGroup(inputChannelName), inputChannel, + this.channelBindingServiceProperties.getConsumerProperties(inputChannelName)); + this.consumerBindings.put(inputChannelName, binding); + return binding; } - public void bindProducer(MessageChannel outputChannel, String outputChannelName) { + public Binding bindProducer(MessageChannel outputChannel, String outputChannelName) { String channelBindingTarget = this.channelBindingServiceProperties.getBindingDestination(outputChannelName); Binder binder = getBinderForChannel(outputChannelName); - if (BinderUtils.isChannelPubSub(channelBindingTarget)) { - binder.bindPubSubProducer(removePrefix(channelBindingTarget), - outputChannel, this.channelBindingServiceProperties.getProducerProperties(outputChannelName)); - } - else { - binder.bindProducer(channelBindingTarget, outputChannel, - this.channelBindingServiceProperties.getProducerProperties(outputChannelName)); - } - } - - private String removePrefix(String bindingTarget) { - Assert.isTrue(StringUtils.hasText(bindingTarget), "Binding target should not be empty/null."); - return bindingTarget.substring(bindingTarget.indexOf(":") + 1); + Binding binding = binder.bindProducer(channelBindingTarget, outputChannel, + this.channelBindingServiceProperties.getProducerProperties(outputChannelName)); + this.producerBindings.put(outputChannelName, binding); + return binding; } public void unbindConsumers(String inputChannelName) { Binder binder = getBinderForChannel(inputChannelName); - if (BinderUtils.isChannelPubSub(this.channelBindingServiceProperties.getBindingDestination(inputChannelName))) { - binder.unbindPubSubConsumers(inputChannelName, consumerGroup(inputChannelName)); - } - else { - binder.unbindConsumers(inputChannelName); - } + binder.unbind(this.consumerBindings.remove(inputChannelName)); } public void unbindProducers(String outputChannelName) { Binder binder = getBinderForChannel(outputChannelName); - binder.unbindProducers(outputChannelName); + binder.unbind(this.producerBindings.remove(outputChannelName)); } private Binder getBinderForChannel(String channelName) { diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ArbitraryInterfaceBindingTestsWithBindingTargets.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ArbitraryInterfaceBindingTestsWithBindingTargets.java index 906d541cb..ad00daa50 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ArbitraryInterfaceBindingTestsWithBindingTargets.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ArbitraryInterfaceBindingTestsWithBindingTargets.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -16,6 +16,7 @@ package org.springframework.cloud.stream.binder; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -54,8 +55,8 @@ public class ArbitraryInterfaceBindingTestsWithBindingTargets { @SuppressWarnings("unchecked") @Test public void testArbitraryInterfaceChannelsBound() { - verify(binder).bindConsumer(eq("someQueue.0"), eq(fooChannels.foo()), Mockito.any()); - verify(binder).bindConsumer(eq("someQueue.1"), eq(fooChannels.bar()), Mockito.any()); + verify(binder).bindConsumer(eq("someQueue.0"), anyString(), eq(fooChannels.foo()), Mockito.any()); + verify(binder).bindConsumer(eq("someQueue.1"), anyString(), eq(fooChannels.bar()), Mockito.any()); verify(binder).bindProducer(eq("someQueue.2"), eq(fooChannels.baz()), Mockito.any()); verify(binder).bindProducer(eq("someQueue.3"), eq(fooChannels.qux()), Mockito.any()); verifyNoMoreInteractions(binder); diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ArbitraryInterfaceBindingTestsWithDefaults.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ArbitraryInterfaceBindingTestsWithDefaults.java index 120cc3112..874f2bb3f 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ArbitraryInterfaceBindingTestsWithDefaults.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ArbitraryInterfaceBindingTestsWithDefaults.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -16,6 +16,7 @@ package org.springframework.cloud.stream.binder; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -53,8 +54,8 @@ public class ArbitraryInterfaceBindingTestsWithDefaults { @SuppressWarnings("unchecked") @Test public void testArbitraryInterfaceChannelsBound() { - verify(binder).bindConsumer(eq("foo"), eq(fooChannels.foo()), Mockito.any()); - verify(binder).bindConsumer(eq("bar"), eq(fooChannels.bar()), Mockito.any()); + verify(binder).bindConsumer(eq("foo"), anyString(), eq(fooChannels.foo()), Mockito.any()); + verify(binder).bindConsumer(eq("bar"), anyString(), eq(fooChannels.bar()), Mockito.any()); verify(binder).bindProducer(eq("baz"), eq(fooChannels.baz()), Mockito.any()); verify(binder).bindProducer(eq("qux"), eq(fooChannels.qux()), Mockito.any()); verifyNoMoreInteractions(binder); diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderAwareChannelResolverTests.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderAwareChannelResolverTests.java index 83963a164..d261fb46b 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderAwareChannelResolverTests.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/BinderAwareChannelResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-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. @@ -20,8 +20,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.doReturn; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -41,7 +42,6 @@ import org.springframework.cloud.stream.binder.local.LocalMessageChannelBinder; import org.springframework.cloud.stream.binding.BinderAwareChannelResolver; import org.springframework.context.support.StaticApplicationContext; import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.channel.PublishSubscribeChannel; import org.springframework.integration.scheduling.PollerMetadata; import org.springframework.integration.support.DefaultMessageBuilderFactory; import org.springframework.integration.support.MessageBuilder; @@ -76,7 +76,7 @@ public class BinderAwareChannelResolverTests { return binder; } }, null); - this.resolver.setBeanFactory(context); + this.resolver.setBeanFactory(context.getBeanFactory()); context.getBeanFactory().registerSingleton("channelResolver", this.resolver); context.registerSingleton("other", DirectChannel.class); @@ -91,8 +91,8 @@ public class BinderAwareChannelResolverTests { } @Test - public void resolveQueueChannel() { - MessageChannel registered = resolver.resolveDestination("queue:foo"); + public void resolveChannel() { + MessageChannel registered = resolver.resolveDestination("foo"); DirectChannel testChannel = new DirectChannel(); final CountDownLatch latch = new CountDownLatch(1); final List> received = new ArrayList>(); @@ -104,7 +104,7 @@ public class BinderAwareChannelResolverTests { latch.countDown(); } }); - binder.bindConsumer("queue:foo", testChannel, null); + binder.bindConsumer("foo", null, testChannel, null); assertEquals(0, received.size()); registered.send(MessageBuilder.withPayload("hello").build()); try { @@ -119,41 +119,6 @@ public class BinderAwareChannelResolverTests { context.close(); } - @Test - public void resolveTopicChannel() { - MessageChannel registered = resolver.resolveDestination("topic:bar"); - PublishSubscribeChannel[] testChannels = { - new PublishSubscribeChannel(), new PublishSubscribeChannel(), new PublishSubscribeChannel() - }; - final CountDownLatch latch = new CountDownLatch(testChannels.length); - final List> received = new ArrayList>(); - for (PublishSubscribeChannel testChannel : testChannels) { - testChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - received.add(message); - latch.countDown(); - } - }); - binder.bindPubSubConsumer("topic:bar", testChannel, null, null); - } - assertEquals(0, received.size()); - registered.send(MessageBuilder.withPayload("hello").build()); - try { - assertTrue("latch timed out", latch.await(1, TimeUnit.SECONDS)); - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - fail("interrupted while awaiting latch"); - } - assertEquals(3, received.size()); - assertEquals("hello", received.get(0).getPayload()); - assertEquals("hello", received.get(1).getPayload()); - assertEquals("hello", received.get(2).getPayload()); - context.close(); - } - @Test public void resolveNonRegisteredChannel() { MessageChannel other = resolver.resolveDestination("other"); @@ -161,12 +126,11 @@ public class BinderAwareChannelResolverTests { } @Test + @SuppressWarnings("rawtypes") public void propertyPassthrough() { Properties properties = new Properties(); - @SuppressWarnings("rawtypes") - Binder binderFactory = mock(Binder.class); - doReturn(new DirectChannel()).when(binderFactory).bindDynamicProducer("queue:foo", properties); - doReturn(new DirectChannel()).when(binderFactory).bindDynamicPubSubProducer("topic:bar", properties); + @SuppressWarnings("unchecked") + Binder binderFactory = mock(Binder.class); BinderFactory mockBinderFactory = Mockito.mock(BinderFactory.class); Mockito.when(mockBinderFactory.getBinder(anyString())).thenReturn(binderFactory); @SuppressWarnings("unchecked") @@ -174,10 +138,12 @@ public class BinderAwareChannelResolverTests { new BinderAwareChannelResolver(mockBinderFactory, properties); BeanFactory beanFactory = new DefaultListableBeanFactory(); resolver.setBeanFactory(beanFactory); - resolver.resolveDestination("queue:foo"); - resolver.resolveDestination("topic:bar"); - verify(binderFactory).bindDynamicProducer("queue:foo", properties); - verify(binderFactory).bindDynamicPubSubProducer("topic:bar", properties); + MessageChannel resolved = resolver.resolveDestination("foo"); + verify(binderFactory).bindProducer(eq("foo"), any(MessageChannel.class), eq(properties)); + assertSame(resolved, beanFactory.getBean("foo")); + resolved = resolver.resolveDestination("someTransport:foo"); + verify(binderFactory).bindProducer(eq("someTransport:foo"), any(MessageChannel.class), eq(properties)); + assertSame(resolved, beanFactory.getBean("someTransport:foo")); } } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/InputOutputBindingOrderTest.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/InputOutputBindingOrderTest.java index 69a7135b8..b735b08ff 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/InputOutputBindingOrderTest.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/InputOutputBindingOrderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -18,6 +18,7 @@ package org.springframework.cloud.stream.binder; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -51,7 +52,7 @@ public class InputOutputBindingOrderTest { Binder binder = applicationContext.getBean(BinderFactory.class).getBinder(null); Processor processor = applicationContext.getBean(Processor.class); // input is bound after the context has been started - verify(binder).bindConsumer(eq("input"), eq(processor.input()), Mockito.any()); + verify(binder).bindConsumer(eq("input"), anyString(), eq(processor.input()), Mockito.any()); SomeLifecycle someLifecycle = applicationContext.getBean(SomeLifecycle.class); assertTrue(someLifecycle.isRunning()); applicationContext.close(); @@ -81,6 +82,7 @@ public class InputOutputBindingOrderTest { private Processor processor; @Override + @SuppressWarnings("unchecked") public synchronized void start() { verify(this.binder).bindProducer(eq("output"), eq(this.processor.output()), Mockito.any()); // input was not bound yet diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithBindingTargets.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithBindingTargets.java index 33320c063..07d18fbb5 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithBindingTargets.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithBindingTargets.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -16,6 +16,7 @@ package org.springframework.cloud.stream.binder; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; @@ -53,7 +54,7 @@ public class ProcessorBindingTestsWithBindingTargets { @SuppressWarnings("unchecked") @Test public void testSourceOutputChannelBound() { - verify(binder).bindConsumer(eq("testtock.0"), eq(testProcessor.input()), Mockito.any()); + verify(binder).bindConsumer(eq("testtock.0"), anyString(), eq(testProcessor.input()), Mockito.any()); verify(binder).bindProducer(eq("testtock.1"), eq(testProcessor.output()), Mockito.any()); } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithDefaults.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithDefaults.java index c86227daa..d1b21f746 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithDefaults.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithDefaults.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -16,6 +16,7 @@ package org.springframework.cloud.stream.binder; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -52,7 +53,7 @@ public class ProcessorBindingTestsWithDefaults { @SuppressWarnings("unchecked") @Test public void testSourceOutputChannelBound() { - Mockito.verify(binder).bindConsumer(eq("input"), eq(processor.input()), Mockito.any()); + Mockito.verify(binder).bindConsumer(eq("input"), anyString(), eq(processor.input()), Mockito.any()); Mockito.verify(binder).bindProducer(eq("output"), eq(processor.output()), Mockito.any()); verifyNoMoreInteractions(binder); } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithPubSubBindingTargets.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithPubSubBindingTargets.java deleted file mode 100644 index df69433f5..000000000 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/ProcessorBindingTestsWithPubSubBindingTargets.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.cloud.stream.binder; - -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -import java.util.Properties; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.cloud.stream.annotation.Bindings; -import org.springframework.cloud.stream.annotation.EnableBinding; -import org.springframework.cloud.stream.messaging.Processor; -import org.springframework.cloud.stream.utils.MockBinderRegistryConfiguration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.PropertySource; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -/** - * @author Marius Bogoevici - */ -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(ProcessorBindingTestsWithPubSubBindingTargets.TestProcessor.class) -public class ProcessorBindingTestsWithPubSubBindingTargets { - - @SuppressWarnings("rawtypes") - @Autowired - private Binder binder; - - @Autowired @Bindings(TestProcessor.class) - private Processor testProcessor; - - @SuppressWarnings("unchecked") - @Test - public void testSourceOutputChannelBound() { - verify(binder).bindPubSubConsumer(eq("testtock.0"), eq(testProcessor.input()), anyString(), - any(Properties.class)); - verify(binder).bindPubSubProducer(eq("testtock.1"), eq(testProcessor.output()), any(Properties.class)); - verifyNoMoreInteractions(binder); - } - - @EnableBinding(Processor.class) - @EnableAutoConfiguration - @Import(MockBinderRegistryConfiguration.class) - @PropertySource("classpath:/org/springframework/cloud/stream/binder/processor-binding-test-pubsub.properties") - public static class TestProcessor { - - } -} diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingPubSubTests.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingPubSubTests.java deleted file mode 100644 index a81261cf2..000000000 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingPubSubTests.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.cloud.stream.binder; - -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -import java.util.Properties; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.cloud.stream.annotation.Bindings; -import org.springframework.cloud.stream.annotation.EnableBinding; -import org.springframework.cloud.stream.messaging.Sink; -import org.springframework.cloud.stream.utils.MockBinderRegistryConfiguration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.PropertySource; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -/** - * @author Marius Bogoevici - * @author Gary Russell - */ -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(SinkBindingPubSubTests.TestSink.class) -public class SinkBindingPubSubTests { - - @SuppressWarnings("rawtypes") - @Autowired - private Binder binder; - - @Autowired @Bindings(TestSink.class) - private Sink testSink; - - @SuppressWarnings("unchecked") - @Test - public void testSourceOutputChannelBound() { - verify(binder).bindPubSubConsumer(eq("testpubsub"), eq(testSink.input()), eq("tgroup"), any(Properties.class)); - verifyNoMoreInteractions(binder); - } - - @EnableBinding(Sink.class) - @EnableAutoConfiguration - @Import(MockBinderRegistryConfiguration.class) - @PropertySource("classpath:/org/springframework/cloud/stream/binder/sink-binding-pubsub-test.properties") - public static class TestSink { - - } - -} diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingTestsWithBindingTargets.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingTestsWithBindingTargets.java index ef6f0f182..5e59a0639 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingTestsWithBindingTargets.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingTestsWithBindingTargets.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -16,6 +16,7 @@ package org.springframework.cloud.stream.binder; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -54,7 +55,7 @@ public class SinkBindingTestsWithBindingTargets { @SuppressWarnings("unchecked") @Test public void testSourceOutputChannelBound() { - verify(binder).bindConsumer(eq("testtock"), eq(testSink.input()), Mockito.any()); + verify(binder).bindConsumer(eq("testtock"), anyString(), eq(testSink.input()), Mockito.any()); verifyNoMoreInteractions(binder); } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingTestsWithDefaults.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingTestsWithDefaults.java index d54c53ba5..285f795b5 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingTestsWithDefaults.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SinkBindingTestsWithDefaults.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -16,6 +16,7 @@ package org.springframework.cloud.stream.binder; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -53,7 +54,7 @@ public class SinkBindingTestsWithDefaults { @SuppressWarnings("unchecked") @Test public void testSourceOutputChannelBound() { - verify(binder).bindConsumer(eq("input"), eq(testSink.input()), Mockito.any()); + verify(binder).bindConsumer(eq("input"), anyString(), eq(testSink.input()), Mockito.any()); verifyNoMoreInteractions(binder); } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/local/LocalMessageChannelBinder.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/local/LocalMessageChannelBinder.java index 83de44c85..4001d515e 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/local/LocalMessageChannelBinder.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/local/LocalMessageChannelBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-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. @@ -17,18 +17,12 @@ package org.springframework.cloud.stream.binder.local; import java.util.Collection; -import java.util.HashMap; -import java.util.Map; import java.util.Properties; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import org.springframework.cloud.stream.binder.AbstractBindingPropertiesAccessor; -import org.springframework.cloud.stream.binder.BinderPropertyKeys; import org.springframework.cloud.stream.binder.Binding; import org.springframework.cloud.stream.binder.MessageChannelBinderSupport; import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.channel.ExecutorChannel; import org.springframework.integration.channel.PublishSubscribeChannel; import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.config.ConsumerEndpointFactoryBean; @@ -37,10 +31,7 @@ import org.springframework.integration.scheduling.PollerMetadata; import org.springframework.integration.support.context.NamedComponent; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandler; -import org.springframework.messaging.MessagingException; import org.springframework.messaging.PollableChannel; -import org.springframework.messaging.SubscribableChannel; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.util.Assert; import org.springframework.util.MimeType; @@ -60,6 +51,8 @@ import org.springframework.util.MimeType; */ public class LocalMessageChannelBinder extends MessageChannelBinderSupport { + public static final String THREAD_NAME_PREFIX = "binder.local-"; + private static final int DEFAULT_EXECUTOR_CORE_POOL_SIZE = 0; private static final int DEFAULT_EXECUTOR_MAX_POOL_SIZE = 200; @@ -68,19 +61,8 @@ public class LocalMessageChannelBinder extends MessageChannelBinderSupport { private static final int DEFAULT_EXECUTOR_KEEPALIVE_SECONDS = 60; - private static final int DEFAULT_REQ_REPLY_CONCURRENCY = 1; - - protected static final Set CONSUMER_REQUEST_REPLY_PROPERTIES = new SetBuilder() - .addAll(CONSUMER_STANDARD_PROPERTIES) - .add(BinderPropertyKeys.CONCURRENCY) - .build(); - - public static final String THREAD_NAME_PREFIX = "binder.local-"; - private volatile PollerMetadata poller; - private final Map requestReplyChannels = new HashMap(); - private final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); private volatile int executorCorePoolSize = DEFAULT_EXECUTOR_CORE_POOL_SIZE; @@ -91,23 +73,6 @@ public class LocalMessageChannelBinder extends MessageChannelBinderSupport { private volatile int executorKeepAliveSeconds = DEFAULT_EXECUTOR_KEEPALIVE_SECONDS; - private volatile int queueSize = Integer.MAX_VALUE; - - private final Map reqRepExecutors = new ConcurrentHashMap<>(); - - /** - * Used to create and customize {@link QueueChannel}s when the binding operation involves aliased names. - */ - private final SharedChannelProvider queueChannelProvider = new SharedChannelProvider( - QueueChannel.class) { - - @Override - protected QueueChannel createSharedChannel(String name) { - QueueChannel queueChannel = new QueueChannel(queueSize); - return queueChannel; - } - }; - private final SharedChannelProvider pubsubChannelProvider = new SharedChannelProvider( PublishSubscribeChannel.class) { @@ -126,13 +91,6 @@ public class LocalMessageChannelBinder extends MessageChannelBinderSupport { this.poller = poller; } - /** - * Set the size of the queue when using {@link QueueChannel}s. - */ - public void setQueueSize(int queueSize) { - this.queueSize = queueSize; - } - /** * Set the {@link ThreadPoolTaskExecutor}} core pool size to limit the number of concurrent * threads. The executor is used for PubSub operations. @@ -183,61 +141,23 @@ public class LocalMessageChannelBinder extends MessageChannelBinderSupport { this.executor.initialize(); } - /** - * For the local binder we bridge the router "output" channel to a queue channel; the queue - * channel gets the name and the source channel is named 'dynamic.output.to.' + name. - * {@inheritDoc} - */ @Override - public MessageChannel bindDynamicProducer(String name, Properties properties) { - return doBindDynamicProducer(name, "dynamic.output.to." + name, properties); - } - - /** - * For the local binder we bridge the router "output" channel to a pub/sub channel; the pub/sub - * channel gets the name and the source channel is named 'dynamic.output.to.' + name. - * {@inheritDoc} - */ - @Override - public MessageChannel bindDynamicPubSubProducer(String name, Properties properties) { - return doBindDynamicPubSubProducer(name, "dynamic.output.to." + name, properties); - } - - private SharedChannelProvider getChannelProvider(String name) { - SharedChannelProvider channelProvider = directChannelProvider; - // Use queue channel provider in case of named channels: - // point-to-point type syntax (queue:) and job input channel syntax (job:) - if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX) || name.startsWith(JOB_CHANNEL_TYPE_PREFIX)) { - channelProvider = queueChannelProvider; - } - return channelProvider; - } - - /** - * Looks up or creates a DirectChannel with the given name and creates a bridge from that channel to the provided - * channel instance. - */ - @Override - public void bindConsumer(String name, MessageChannel moduleInputChannel, Properties properties) { - validateConsumerProperties(name, properties, CONSUMER_STANDARD_PROPERTIES); - doRegisterConsumer(name, moduleInputChannel, getChannelProvider(name), properties); - } - - @Override - public void bindPubSubConsumer(String name, MessageChannel moduleInputChannel, String group, + protected Binding doBindConsumer(String name, String group, MessageChannel moduleInputChannel, Properties properties) { validateConsumerProperties(name, properties, CONSUMER_STANDARD_PROPERTIES); - doRegisterConsumer(name, moduleInputChannel, this.pubsubChannelProvider, properties); + return doRegisterConsumer(name, moduleInputChannel, this.pubsubChannelProvider, properties); } - private void doRegisterConsumer(String name, MessageChannel moduleInputChannel, + private Binding doRegisterConsumer(String name, MessageChannel moduleInputChannel, SharedChannelProvider channelProvider, Properties properties) { Assert.hasText(name, "a valid name is required to register an inbound channel"); Assert.notNull(moduleInputChannel, "channel must not be null"); - MessageChannel registeredChannel = channelProvider.lookupOrCreateSharedChannel(name); + MessageChannel registeredChannel = channelProvider.lookupOrCreateSharedChannel("localbinder." + name); bridge(name, registeredChannel, moduleInputChannel, "inbound." + ((NamedComponent) registeredChannel).getComponentName(), new LocalBindingPropertiesAccessor(properties)); + // TODO: ? + return null; } /** @@ -245,109 +165,25 @@ public class LocalMessageChannelBinder extends MessageChannelBinderSupport { * channel instance. */ @Override - public void bindProducer(String name, MessageChannel moduleOutputChannel, Properties properties) { + public Binding bindProducer(String name, MessageChannel moduleOutputChannel, Properties properties) { validateConsumerProperties(name, properties, PRODUCER_STANDARD_PROPERTIES); - doRegisterProducer(name, moduleOutputChannel, getChannelProvider(name), properties); + return doRegisterProducer(name, moduleOutputChannel, this.pubsubChannelProvider, properties); } - @Override - public void bindPubSubProducer(String name, MessageChannel moduleOutputChannel, - Properties properties) { - validateConsumerProperties(name, properties, PRODUCER_STANDARD_PROPERTIES); - doRegisterProducer(name, moduleOutputChannel, this.pubsubChannelProvider, properties); - } - - private void doRegisterProducer(String name, MessageChannel moduleOutputChannel, + private Binding doRegisterProducer(String name, MessageChannel moduleOutputChannel, SharedChannelProvider channelProvider, Properties properties) { Assert.hasText(name, "a valid name is required to register an outbound channel"); Assert.notNull(moduleOutputChannel, "channel must not be null"); - MessageChannel registeredChannel = channelProvider.lookupOrCreateSharedChannel(name); + MessageChannel registeredChannel = channelProvider.lookupOrCreateSharedChannel("localbinder." + name); bridge(name, moduleOutputChannel, registeredChannel, "outbound." + ((NamedComponent) registeredChannel).getComponentName(), new LocalBindingPropertiesAccessor(properties)); + // TODO: ? + return null; } @Override - public void bindRequestor(final String name, MessageChannel requests, final MessageChannel replies, - Properties properties) { - validateConsumerProperties(name, properties, CONSUMER_REQUEST_REPLY_PROPERTIES); - final MessageChannel requestChannel = this.findOrCreateRequestReplyChannel(name, "requestor.", properties); - Assert.isInstanceOf(SubscribableChannel.class, requests); - ((SubscribableChannel) requests).subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - requestChannel.send(message); - } - }); - - ExecutorChannel replyChannel = this.findOrCreateRequestReplyChannel(name, "replier.", properties); - replyChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - replies.send(message); - } - }); - } - - @Override - public void bindReplier(String name, final MessageChannel requests, MessageChannel replies, - Properties properties) { - validateConsumerProperties(name, properties, CONSUMER_REQUEST_REPLY_PROPERTIES); - SubscribableChannel requestChannel = this.findOrCreateRequestReplyChannel(name, "requestor.", properties); - requestChannel.subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - requests.send(message); - } - }); - - Assert.isInstanceOf(SubscribableChannel.class, replies); - final SubscribableChannel replyChannel = this.findOrCreateRequestReplyChannel(name, "replier.", properties); - ((SubscribableChannel) replies).subscribe(new MessageHandler() { - - @Override - public void handleMessage(Message message) throws MessagingException { - replyChannel.send(message); - } - }); - } - - private synchronized ExecutorChannel findOrCreateRequestReplyChannel(String name, String prefix, - Properties properties) { - String channelName = prefix + name; - ExecutorChannel channel = this.requestReplyChannels.get(channelName); - if (channel == null) { - ThreadPoolTaskExecutor executor = createRequestReplyExecutor(name, properties); - channel = new ExecutorChannel(executor); - channel.setBeanFactory(getBeanFactory()); - this.requestReplyChannels.put(channelName, channel); - this.reqRepExecutors.put(name, executor); - } - return channel; - } - - private ThreadPoolTaskExecutor createRequestReplyExecutor(String name, Properties properties) { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(new LocalBindingPropertiesAccessor(properties).getConcurrency(DEFAULT_REQ_REPLY_CONCURRENCY)); - executor.setThreadNamePrefix(THREAD_NAME_PREFIX + name + "-"); - executor.initialize(); - return executor; - } - - @Override - public void unbindProducer(String name, MessageChannel channel) { - this.requestReplyChannels.remove("replier." + name); - MessageChannel requestChannel = this.requestReplyChannels.remove("requestor." + name); - if (requestChannel == null) { - super.unbindProducer(name, channel); - } - ThreadPoolTaskExecutor executor = this.reqRepExecutors.remove(name); - if (executor != null) { - executor.shutdown(); - } + public void unbind(Binding binding) { } protected BridgeHandler bridge(String name, MessageChannel from, MessageChannel to, String bridgeName, @@ -397,7 +233,7 @@ public class LocalMessageChannelBinder extends MessageChannelBinderSupport { try { cefb.getObject().setComponentName(handler.getComponentName()); - Binding binding = isInbound ? Binding.forConsumer(name, cefb.getObject(), to, properties) + Binding binding = isInbound ? Binding.forConsumer(name, null, cefb.getObject(), to, properties) : Binding.forProducer(name, from, cefb.getObject(), properties); addBinding(binding); binding.start(); diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub1/StubBinder1.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub1/StubBinder1.java index 833fe9666..8089684c2 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub1/StubBinder1.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub1/StubBinder1.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -19,11 +19,13 @@ package org.springframework.cloud.stream.binder.stub1; import java.util.Properties; import org.springframework.cloud.stream.binder.Binder; +import org.springframework.cloud.stream.binder.Binding; /** * @author Marius Bogoevici + * @author Mark Fisher */ -public class StubBinder1 implements Binder { +public class StubBinder1 implements Binder { private String name; @@ -36,67 +38,17 @@ public class StubBinder1 implements Binder { } @Override - public void bindConsumer(String name, Object inboundBindTarget, Properties properties) { - - } - - @Override - public void bindPubSubConsumer(String name, Object inboundBindTarget, String group, Properties properties) { - - } - - @Override - public void bindProducer(String name, Object outboundBindTarget, Properties properties) { - - } - - @Override - public void bindPubSubProducer(String name, Object outboundBindTarget, Properties properties) { - - } - - @Override - public void unbindConsumers(String name) { - - } - - @Override - public void unbindPubSubConsumers(String name, String group) { - - } - - @Override - public void unbindProducers(String name) { - - } - - @Override - public void unbindConsumer(String name, Object inboundBindTarget) { - - } - - @Override - public void unbindProducer(String name, Object outboundBindTarget) { - - } - - @Override - public void bindRequestor(String name, Object requests, Object replies, Properties properties) { - - } - - @Override - public void bindReplier(String name, Object requests, Object replies, Properties properties) { - - } - - @Override - public Object bindDynamicProducer(String name, Properties properties) { + public Binding bindConsumer(String name, String group, Object inboundBindTarget, Properties properties) { return null; } @Override - public Object bindDynamicPubSubProducer(String name, Properties properties) { + public Binding bindProducer(String name, Object outboundBindTarget, Properties properties) { return null; } + + @Override + public void unbind(Binding binding) { + } + } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub1/StubBinder1Configuration.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub1/StubBinder1Configuration.java index b6a625ae0..dc2eb9ac2 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub1/StubBinder1Configuration.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub1/StubBinder1Configuration.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -31,7 +31,7 @@ public class StubBinder1Configuration { @Bean @ConfigurationProperties("binder1") - public Binder binder() { + public Binder binder() { return new StubBinder1(); } } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub2/StubBinder2.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub2/StubBinder2.java index f20c4e0c8..a4cf2e307 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub2/StubBinder2.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub2/StubBinder2.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -19,80 +19,33 @@ package org.springframework.cloud.stream.binder.stub2; import java.util.Properties; import org.springframework.cloud.stream.binder.Binder; +import org.springframework.cloud.stream.binder.Binding; /** * @author Marius Bogoevici + * @author Mark Fisher */ -public class StubBinder2 implements Binder { +public class StubBinder2 implements Binder { - private StubBinder2Dependency stubBinder2Dependency; + @SuppressWarnings("unused") + private final StubBinder2Dependency stubBinder2Dependency; public StubBinder2(StubBinder2Dependency stubBinder2Dependency) { this.stubBinder2Dependency = stubBinder2Dependency; } @Override - public void bindConsumer(String name, Object inboundBindTarget, Properties properties) { - - } - - @Override - public void bindPubSubConsumer(String name, Object inboundBindTarget, String group, Properties properties) { - - } - - @Override - public void bindProducer(String name, Object outboundBindTarget, Properties properties) { - - } - - @Override - public void bindPubSubProducer(String name, Object outboundBindTarget, Properties properties) { - - } - - @Override - public void unbindConsumers(String name) { - - } - - @Override - public void unbindPubSubConsumers(String name, String group) { - - } - - @Override - public void unbindProducers(String name) { - - } - - @Override - public void unbindConsumer(String name, Object inboundBindTarget) { - - } - - @Override - public void unbindProducer(String name, Object outboundBindTarget) { - - } - - @Override - public void bindRequestor(String name, Object requests, Object replies, Properties properties) { - - } - - @Override - public void bindReplier(String name, Object requests, Object replies, Properties properties) { - - } - - @Override - public Object bindDynamicProducer(String name, Properties properties) { + public Binding bindConsumer(String name, String group, Object inboundBindTarget, Properties properties) { return null; } @Override - public Object bindDynamicPubSubProducer(String name, Properties properties) { + public Binding bindProducer(String name, Object outboundBindTarget, Properties properties) { return null; } + + @Override + public void unbind(Binding binding) { + } + } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub2/StubBinder2ConfigurationA.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub2/StubBinder2ConfigurationA.java index 5f896ef6f..1043ee9d2 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub2/StubBinder2ConfigurationA.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/stub2/StubBinder2ConfigurationA.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -28,7 +28,7 @@ import org.springframework.context.annotation.Configuration; public class StubBinder2ConfigurationA { @Bean - public Binder binder(StubBinder2Dependency dependency) { + public Binder binder(StubBinder2Dependency dependency) { return new StubBinder2(dependency); } } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binding/ChannelBindingServiceTests.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binding/ChannelBindingServiceTests.java index 61fb120b2..761462cbf 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binding/ChannelBindingServiceTests.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binding/ChannelBindingServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.cloud.stream.binding; import static org.mockito.Mockito.verify; @@ -27,6 +28,7 @@ import org.junit.Test; import org.springframework.cloud.stream.binder.Binder; import org.springframework.cloud.stream.binder.BinderConfiguration; import org.springframework.cloud.stream.binder.BinderType; +import org.springframework.cloud.stream.binder.Binding; import org.springframework.cloud.stream.binder.DefaultBinderFactory; import org.springframework.cloud.stream.config.BindingProperties; import org.springframework.cloud.stream.config.ChannelBindingServiceProperties; @@ -36,12 +38,12 @@ import org.springframework.messaging.MessageChannel; /** * @author Gary Russell - * + * @author Mark Fisher */ public class ChannelBindingServiceTests { @Test - public void testSimple() throws Exception { + public void testDefaultGroup() throws Exception { ChannelBindingServiceProperties properties = new ChannelBindingServiceProperties(); Map bindings = new HashMap<>(); BindingProperties props = new BindingProperties(); @@ -49,7 +51,6 @@ public class ChannelBindingServiceTests { String name = "foo"; bindings.put(name, props); properties.setBindings(bindings); - @SuppressWarnings("unchecked") DefaultBinderFactory binderFactory = new DefaultBinderFactory<>(Collections.singletonMap("mock", new BinderConfiguration(new BinderType("mock", new Class[]{MockBinderConfiguration.class}), @@ -57,23 +58,22 @@ public class ChannelBindingServiceTests { Binder binder = binderFactory.getBinder("mock"); ChannelBindingService service = new ChannelBindingService(properties, binderFactory); MessageChannel inputChannel = new DirectChannel(); - service.bindConsumer(inputChannel, name); + Binding binding = service.bindConsumer(inputChannel, name); service.unbindConsumers(name); - verify(binder).bindConsumer(name, inputChannel, properties.getConsumerProperties(name)); - verify(binder).unbindConsumers(name); + verify(binder).bindConsumer(name, props.getGroup(), inputChannel, properties.getConsumerProperties(name)); + verify(binder).unbind(binding); binderFactory.destroy(); } @Test - public void testPubSub() throws Exception { + public void testExplicitGroup() throws Exception { ChannelBindingServiceProperties properties = new ChannelBindingServiceProperties(); Map bindings = new HashMap<>(); BindingProperties props = new BindingProperties(); - props.setDestination("topic:foo"); + props.setDestination("foo"); String name = "foo"; bindings.put(name, props); properties.setBindings(bindings); - @SuppressWarnings("unchecked") DefaultBinderFactory binderFactory = new DefaultBinderFactory<>(Collections.singletonMap("mock", new BinderConfiguration(new BinderType("mock", new Class[]{MockBinderConfiguration.class}), @@ -81,10 +81,10 @@ public class ChannelBindingServiceTests { Binder binder = binderFactory.getBinder("mock"); ChannelBindingService service = new ChannelBindingService(properties, binderFactory); MessageChannel inputChannel = new DirectChannel(); - service.bindConsumer(inputChannel, name); + Binding binding = service.bindConsumer(inputChannel, name); service.unbindConsumers(name); - verify(binder).bindPubSubConsumer(name, inputChannel, props.getGroup(), properties.getConsumerProperties(name)); - verify(binder).unbindPubSubConsumers(name, props.getGroup()); + verify(binder).bindConsumer(name, props.getGroup(), inputChannel, properties.getConsumerProperties(name)); + verify(binder).unbind(binding); binderFactory.destroy(); } diff --git a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/partitioning/PartitionedConsumerTest.java b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/partitioning/PartitionedConsumerTest.java index 6aed10986..a7a29aa9d 100644 --- a/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/partitioning/PartitionedConsumerTest.java +++ b/spring-cloud-stream/src/test/java/org/springframework/cloud/stream/partitioning/PartitionedConsumerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-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. @@ -17,6 +17,7 @@ package org.springframework.cloud.stream.partitioning; import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -61,10 +62,9 @@ public class PartitionedConsumerTest { @SuppressWarnings("unchecked") public void testBindingPartitionedConsumer() { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Properties.class); - verify(binder).bindConsumer(eq("partIn"), eq(testSink.input()), argumentCaptor.capture()); + verify(binder).bindConsumer(eq("partIn"), anyString(), eq(testSink.input()), argumentCaptor.capture()); Assert.assertThat(argumentCaptor.getValue().getProperty(BinderPropertyKeys.PARTITION_INDEX), equalTo("0")); - Assert.assertThat(argumentCaptor.getValue().getProperty(BinderPropertyKeys.COUNT), - equalTo("2")); + Assert.assertThat(argumentCaptor.getValue().getProperty(BinderPropertyKeys.COUNT), equalTo("2")); verifyNoMoreInteractions(binder); }