XD-3242 created spring-cloud-streams-codec
Port MessageBus code from XD (XD-3244) Fix Deprecated API Usage; Fix Imports (GH-9) Change poms to compile using JDK7 Move transport specific test classes into binding-<transport> projects
This commit is contained in:
committed by
Mark Pollack
parent
cb75353aeb
commit
5377eaf258
@@ -10,7 +10,6 @@
|
||||
<artifactId>spring-cloud-streams-docs</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>Spring Cloud Streams Docs</name>
|
||||
<version>1.1.0.BUILD-SNAPSHOT</version>
|
||||
<description>Spring Cloud Docs</description>
|
||||
<properties>
|
||||
<docs.main>spring-cloud-streams</docs.main>
|
||||
|
||||
27
pom.xml
27
pom.xml
@@ -27,6 +27,7 @@
|
||||
</properties>
|
||||
<modules>
|
||||
<module>spring-cloud-streams</module>
|
||||
<module>spring-cloud-streams-bindings</module>
|
||||
<module>spring-cloud-streams-codec</module>
|
||||
<module>spring-cloud-streams-common</module>
|
||||
<module>spring-xd-runner</module>
|
||||
@@ -75,19 +76,19 @@
|
||||
<version>${spring-xd.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-messagebus-local</artifactId>
|
||||
<version>${spring-xd.version}</version>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-local</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-messagebus-redis</artifactId>
|
||||
<version>${spring-xd.version}</version>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-redis</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-messagebus-rabbit</artifactId>
|
||||
<version>${spring-xd.version}</version>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-rabbit</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
@@ -118,6 +119,14 @@
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>jcenter</id>
|
||||
<name>JCenter Bintray</name>
|
||||
<url>http://jcenter.bintray.com</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
|
||||
95
spring-cloud-streams-bindings/pom.xml
Normal file
95
spring-cloud-streams-bindings/pom.xml
Normal file
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-streams-bindings-parent</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<url>http://projects.spring.io/spring-xd/</url>
|
||||
<organization>
|
||||
<name>Pivotal Software, Inc.</name>
|
||||
<url>http://www.spring.io</url>
|
||||
</organization>
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-parent</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
<properties>
|
||||
<kafka.version>0.8.2.1</kafka.version>
|
||||
<curator.version>2.6.0</curator.version>
|
||||
</properties>
|
||||
<modules>
|
||||
<module>spring-cloud-streams-binding-spi</module>
|
||||
<module>spring-cloud-streams-binding-test</module>
|
||||
<module>spring-cloud-streams-binding-local</module>
|
||||
<module>spring-cloud-streams-binding-rabbit</module>
|
||||
<module>spring-cloud-streams-binding-redis</module>
|
||||
<module>spring-cloud-streams-binding-kafka</module>
|
||||
</modules>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-spi</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-test</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.kafka</groupId>
|
||||
<artifactId>kafka_2.10</artifactId>
|
||||
<version>${kafka.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-log4j12</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.kafka</groupId>
|
||||
<artifactId>kafka_2.10</artifactId>
|
||||
<classifier>test</classifier>
|
||||
<version>${kafka.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.kafka</groupId>
|
||||
<artifactId>kafka-clients</artifactId>
|
||||
<version>${kafka.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.curator</groupId>
|
||||
<artifactId>curator-framework</artifactId>
|
||||
<version>${curator.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.curator</groupId>
|
||||
<artifactId>curator-recipes</artifactId>
|
||||
<version>${curator.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.curator</groupId>
|
||||
<artifactId>curator-test</artifactId>
|
||||
<version>${curator.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-logging</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-streams-binding-kafka</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>spring-cloud-streams-binding-kafka</name>
|
||||
<description>Kafka binding implementation</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-bindings-parent</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<kafka.version>0.8.2.1</kafka.version>
|
||||
<spring-integration-kafka.version>1.2.0.RELEASE</spring-integration-kafka.version>
|
||||
<rxjava.version>1.0.0</rxjava.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-spi</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-test</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.integration</groupId>
|
||||
<artifactId>spring-integration-kafka</artifactId>
|
||||
<version>${spring-integration-kafka.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.avro</groupId>
|
||||
<artifactId>avro-compiler</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.kafka</groupId>
|
||||
<artifactId>kafka_2.10</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.kafka</groupId>
|
||||
<artifactId>kafka-clients</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.reactivex</groupId>
|
||||
<artifactId>rxjava</artifactId>
|
||||
<version>${rxjava.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.reactivex</groupId>
|
||||
<artifactId>rxjava-math</artifactId>
|
||||
<version>${rxjava.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-tuple</artifactId>
|
||||
<version>${spring-xd.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-codec</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.curator</groupId>
|
||||
<artifactId>curator-recipes</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.kafka</groupId>
|
||||
<artifactId>kafka_2.10</artifactId>
|
||||
<classifier>test</classifier>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.curator</groupId>
|
||||
<artifactId>curator-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.kafka;
|
||||
|
||||
import kafka.serializer.Decoder;
|
||||
import kafka.serializer.Encoder;
|
||||
import kafka.utils.VerifiableProperties;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
|
||||
/**
|
||||
* A Kafka encoder / decoder used to serialize a single int, used as the kafka partition key.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class IntegerEncoderDecoder implements Encoder<Integer>, Decoder<Integer> {
|
||||
|
||||
|
||||
public IntegerEncoderDecoder() {
|
||||
this(new VerifiableProperties());
|
||||
}
|
||||
|
||||
public IntegerEncoderDecoder(VerifiableProperties properties) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer fromBytes(byte[] bytes) {
|
||||
Assert.isTrue(bytes.length == 4);
|
||||
return bytes[0] << 24 | (bytes[1] & 0xFF) << 16 | (bytes[2] & 0xFF) << 8 | (bytes[3] & 0xFF);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBytes(Integer message) {
|
||||
int value = message.intValue();
|
||||
return new byte[] {
|
||||
(byte) (value >>> 24),
|
||||
(byte) (value >>> 16),
|
||||
(byte) (value >>> 8),
|
||||
(byte) value
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,947 @@
|
||||
/*
|
||||
* Copyright 2014-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.kafka;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import kafka.admin.AdminUtils;
|
||||
import kafka.api.OffsetRequest;
|
||||
import kafka.serializer.Decoder;
|
||||
import kafka.serializer.DefaultDecoder;
|
||||
import kafka.utils.ZkUtils;
|
||||
|
||||
import org.I0Itec.zkclient.ZkClient;
|
||||
import org.I0Itec.zkclient.exception.ZkMarshallingError;
|
||||
import org.I0Itec.zkclient.serialize.ZkSerializer;
|
||||
import org.apache.kafka.clients.producer.ProducerConfig;
|
||||
import org.apache.kafka.common.serialization.ByteArraySerializer;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.integration.channel.FixedSubscriberChannel;
|
||||
import org.springframework.integration.endpoint.EventDrivenConsumer;
|
||||
import org.springframework.integration.handler.AbstractMessageHandler;
|
||||
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
|
||||
import org.springframework.integration.kafka.core.ConnectionFactory;
|
||||
import org.springframework.integration.kafka.core.DefaultConnectionFactory;
|
||||
import org.springframework.integration.kafka.core.Partition;
|
||||
import org.springframework.integration.kafka.core.ZookeeperConfiguration;
|
||||
import org.springframework.integration.kafka.inbound.KafkaMessageDrivenChannelAdapter;
|
||||
import org.springframework.integration.kafka.listener.Acknowledgment;
|
||||
import org.springframework.integration.kafka.listener.KafkaMessageListenerContainer;
|
||||
import org.springframework.integration.kafka.listener.KafkaTopicOffsetManager;
|
||||
import org.springframework.integration.kafka.listener.OffsetManager;
|
||||
import org.springframework.integration.kafka.support.KafkaHeaders;
|
||||
import org.springframework.integration.kafka.support.ProducerConfiguration;
|
||||
import org.springframework.integration.kafka.support.ProducerFactoryBean;
|
||||
import org.springframework.integration.kafka.support.ProducerMetadata;
|
||||
import org.springframework.integration.kafka.support.ZookeeperConnect;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.SubscribableChannel;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.retry.RetryCallback;
|
||||
import org.springframework.retry.RetryContext;
|
||||
import org.springframework.retry.RetryOperations;
|
||||
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
|
||||
import org.springframework.retry.policy.SimpleRetryPolicy;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.xd.dirt.integration.bus.AbstractBusPropertiesAccessor;
|
||||
import org.springframework.xd.dirt.integration.bus.Binding;
|
||||
import org.springframework.xd.dirt.integration.bus.BusProperties;
|
||||
import org.springframework.xd.dirt.integration.bus.EmbeddedHeadersMessageConverter;
|
||||
import org.springframework.xd.dirt.integration.bus.MessageBusSupport;
|
||||
import org.springframework.xd.dirt.integration.bus.MessageValues;
|
||||
import org.springframework.xd.dirt.integration.bus.XdHeaders;
|
||||
import org.springframework.xd.dirt.integration.bus.serializer.MultiTypeCodec;
|
||||
|
||||
import scala.collection.Seq;
|
||||
|
||||
/**
|
||||
* A message bus that uses Kafka as the underlying middleware. The general implementation mapping between XD concepts
|
||||
* and Kafka concepts is as follows:
|
||||
* A message bus that uses Kafka as the underlying middleware.
|
||||
* The general implementation mapping between XD concepts and Kafka concepts is as follows:
|
||||
* <table>
|
||||
* <tr>
|
||||
* <th>Stream definition</th><th>Kafka topic</th><th>Kafka partitions</th><th>Notes</th>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>foo = "http | log"</td><td>foo.0</td><td>1 partition</td><td>1 producer, 1 consumer</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>foo = "http | log", log.count=x</td><td>foo.0</td><td>x partitions</td><td>1 producer, x consumers with static
|
||||
* group 'springXD', achieves queue semantics</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>foo = "http | log", log.count=x + XD partitioning</td><td>still 1 topic 'foo.0'</td><td>x partitions + use key
|
||||
* computed by XD</td><td>1 producer, x consumers with static group 'springXD', achieves queue semantics</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>foo = "http | log", log.count=x, concurrency=y</td><td>foo.0</td><td>x*y partitions</td><td>1 producer, x XD
|
||||
* consumers, each with y threads</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>foo = "http | log", log.count=0, x actual log containers</td><td>foo.0</td><td>10(configurable)
|
||||
* partitions</td><td>1 producer, x XD consumers. Can't know the number of partitions beforehand, so decide a number
|
||||
* that better be greater than number of containers</td>
|
||||
* </tr>
|
||||
* </table>
|
||||
* @author Eric Bottard
|
||||
* @author Marius Bogoevici
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @author David Turanski
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class KafkaMessageBus extends MessageBusSupport {
|
||||
|
||||
public static final ByteArraySerializer BYTE_ARRAY_SERIALIZER = new ByteArraySerializer();
|
||||
|
||||
public static final int METADATA_VERIFICATION_RETRY_ATTEMPTS = 10;
|
||||
|
||||
public static final double METADATA_VERIFICATION_RETRY_BACKOFF_MULTIPLIER = 2;
|
||||
|
||||
public static final int METADATA_VERIFICATION_RETRY_INITIAL_INTERVAL = 100;
|
||||
|
||||
public static final int METADATA_VERIFICATION_MAX_INTERVAL = 1000;
|
||||
|
||||
public static final String FETCH_SIZE = "fetchSize";
|
||||
|
||||
public static final String QUEUE_SIZE = "fetchSize";
|
||||
|
||||
public static final String REQUIRED_ACKS = "requiredAcks";
|
||||
|
||||
public static final String COMPRESSION_CODEC = "compressionCodec";
|
||||
|
||||
public static final String AUTO_COMMIT_ENABLED = "autoCommitEnabled";
|
||||
|
||||
private static final String DEFAULT_COMPRESSION_CODEC = "none";
|
||||
|
||||
private static final int DEFAULT_REQUIRED_ACKS = 1;
|
||||
|
||||
private static final boolean DEFAULT_AUTO_COMMIT_ENABLED = true;
|
||||
|
||||
private RetryOperations retryOperations;
|
||||
|
||||
/**
|
||||
* Used when writing directly to ZK. This is what Kafka expects.
|
||||
*/
|
||||
public final static ZkSerializer utf8Serializer = new ZkSerializer() {
|
||||
|
||||
@Override
|
||||
public byte[] serialize(Object data) throws ZkMarshallingError {
|
||||
try {
|
||||
return ((String) data).getBytes("UTF-8");
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
throw new ZkMarshallingError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object deserialize(byte[] bytes) throws ZkMarshallingError {
|
||||
try {
|
||||
return new String(bytes, "UTF-8");
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
throw new ZkMarshallingError(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected static final Set<Object> PRODUCER_COMPRESSION_PROPERTIES = new HashSet<Object>(
|
||||
Arrays.asList(new String[] {
|
||||
KafkaMessageBus.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<Object> KAFKA_CONSUMER_PROPERTIES = new SetBuilder()
|
||||
.add(BusProperties.MIN_PARTITION_COUNT)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Basic + concurrency + partitioning.
|
||||
*/
|
||||
private static final Set<Object> SUPPORTED_CONSUMER_PROPERTIES = new SetBuilder()
|
||||
.addAll(CONSUMER_STANDARD_PROPERTIES)
|
||||
.addAll(KAFKA_CONSUMER_PROPERTIES)
|
||||
.add(BusProperties.PARTITION_INDEX) // Not actually used
|
||||
.add(BusProperties.COUNT) // Not actually used
|
||||
.add(BusProperties.CONCURRENCY)
|
||||
.add(FETCH_SIZE)
|
||||
.build();
|
||||
|
||||
private static final Set<Object> KAFKA_PRODUCER_PROPERTIES = new SetBuilder()
|
||||
.add(BusProperties.MIN_PARTITION_COUNT)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Basic + concurrency.
|
||||
*/
|
||||
private static final Set<Object> SUPPORTED_NAMED_CONSUMER_PROPERTIES = new SetBuilder()
|
||||
.addAll(CONSUMER_STANDARD_PROPERTIES)
|
||||
.build();
|
||||
|
||||
private static final Set<Object> SUPPORTED_NAMED_PRODUCER_PROPERTIES = new SetBuilder()
|
||||
.addAll(PRODUCER_STANDARD_PROPERTIES)
|
||||
.addAll(PRODUCER_BATCHING_BASIC_PROPERTIES)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Partitioning + kafka producer properties.
|
||||
*/
|
||||
private static final Set<Object> SUPPORTED_PRODUCER_PROPERTIES = new SetBuilder()
|
||||
.addAll(PRODUCER_PARTITIONING_PROPERTIES)
|
||||
.addAll(PRODUCER_STANDARD_PROPERTIES)
|
||||
.add(BusProperties.DIRECT_BINDING_ALLOWED)
|
||||
.addAll(KAFKA_PRODUCER_PROPERTIES)
|
||||
.addAll(PRODUCER_BATCHING_BASIC_PROPERTIES)
|
||||
.addAll(PRODUCER_COMPRESSION_PROPERTIES)
|
||||
.build();
|
||||
|
||||
private final EmbeddedHeadersMessageConverter embeddedHeadersMessageConverter = new
|
||||
EmbeddedHeadersMessageConverter();
|
||||
|
||||
private final ZookeeperConnect zookeeperConnect;
|
||||
|
||||
private String brokers;
|
||||
|
||||
private String[] headersToMap;
|
||||
|
||||
private String zkAddress;
|
||||
|
||||
// -------- Default values for properties -------
|
||||
private int defaultReplicationFactor = 1;
|
||||
|
||||
private String defaultCompressionCodec = DEFAULT_COMPRESSION_CODEC;
|
||||
|
||||
private int defaultRequiredAcks = DEFAULT_REQUIRED_ACKS;
|
||||
|
||||
private int defaultQueueSize = 1024;
|
||||
|
||||
private int defaultMaxWait = 100;
|
||||
|
||||
private int defaultFetchSize = 1024 * 1024;
|
||||
|
||||
private int defaultMinPartitionCount = 1;
|
||||
|
||||
private ConnectionFactory connectionFactory;
|
||||
|
||||
private String offsetStoreTopic = "SpringXdOffsets";
|
||||
|
||||
// auto commit property
|
||||
|
||||
private boolean defaultAutoCommitEnabled = DEFAULT_AUTO_COMMIT_ENABLED;
|
||||
|
||||
private int socketBufferSize = 2097152;
|
||||
|
||||
private int offsetStoreSegmentSize = 250 * 1024 * 1024;
|
||||
|
||||
private int offsetStoreRetentionTime = 60000;
|
||||
|
||||
private int offsetStoreRequiredAcks = 1;
|
||||
|
||||
private int offsetStoreMaxFetchSize = 1048576;
|
||||
|
||||
private int offsetStoreBatchBytes = 200;
|
||||
|
||||
private int offsetStoreBatchTime = 1000;
|
||||
|
||||
private int offsetUpdateTimeWindow = 10000;
|
||||
|
||||
private int offsetUpdateCount = 0;
|
||||
|
||||
private int offsetUpdateShutdownTimeout = 2000;
|
||||
|
||||
private Mode mode = Mode.embeddedHeaders;
|
||||
|
||||
public KafkaMessageBus(ZookeeperConnect zookeeperConnect, String brokers, String zkAddress,
|
||||
MultiTypeCodec<Object> codec, String... headersToMap) {
|
||||
this.zookeeperConnect = zookeeperConnect;
|
||||
this.brokers = brokers;
|
||||
this.zkAddress = zkAddress;
|
||||
setCodec(codec);
|
||||
if (headersToMap.length > 0) {
|
||||
String[] combinedHeadersToMap =
|
||||
Arrays.copyOfRange(XdHeaders.STANDARD_HEADERS, 0, XdHeaders.STANDARD_HEADERS.length + headersToMap
|
||||
.length);
|
||||
System.arraycopy(headersToMap, 0, combinedHeadersToMap, XdHeaders.STANDARD_HEADERS.length, headersToMap
|
||||
.length);
|
||||
this.headersToMap = combinedHeadersToMap;
|
||||
}
|
||||
else {
|
||||
this.headersToMap = XdHeaders.STANDARD_HEADERS;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void setOffsetStoreTopic(String offsetStoreTopic) {
|
||||
this.offsetStoreTopic = offsetStoreTopic;
|
||||
}
|
||||
|
||||
public void setOffsetStoreSegmentSize(int offsetStoreSegmentSize) {
|
||||
this.offsetStoreSegmentSize = offsetStoreSegmentSize;
|
||||
}
|
||||
|
||||
public void setOffsetStoreRetentionTime(int offsetStoreRetentionTime) {
|
||||
this.offsetStoreRetentionTime = offsetStoreRetentionTime;
|
||||
}
|
||||
|
||||
public void setSocketBufferSize(int socketBufferSize) {
|
||||
this.socketBufferSize = socketBufferSize;
|
||||
}
|
||||
|
||||
public void setOffsetStoreRequiredAcks(int offsetStoreRequiredAcks) {
|
||||
this.offsetStoreRequiredAcks = offsetStoreRequiredAcks;
|
||||
}
|
||||
|
||||
public void setOffsetStoreMaxFetchSize(int offsetStoreMaxFetchSize) {
|
||||
this.offsetStoreMaxFetchSize = offsetStoreMaxFetchSize;
|
||||
}
|
||||
|
||||
|
||||
public void setOffsetUpdateTimeWindow(int offsetUpdateTimeWindow) {
|
||||
this.offsetUpdateTimeWindow = offsetUpdateTimeWindow;
|
||||
}
|
||||
|
||||
public void setOffsetUpdateCount(int offsetUpdateCount) {
|
||||
this.offsetUpdateCount = offsetUpdateCount;
|
||||
}
|
||||
|
||||
public void setOffsetUpdateShutdownTimeout(int offsetUpdateShutdownTimeout) {
|
||||
this.offsetUpdateShutdownTimeout = offsetUpdateShutdownTimeout;
|
||||
}
|
||||
|
||||
public void setOffsetStoreBatchBytes(int offsetStoreBatchBytes) {
|
||||
this.offsetStoreBatchBytes = offsetStoreBatchBytes;
|
||||
}
|
||||
|
||||
public void setOffsetStoreBatchTime(int offsetStoreBatchTime) {
|
||||
this.offsetStoreBatchTime = offsetStoreBatchTime;
|
||||
}
|
||||
|
||||
public ConnectionFactory getConnectionFactory() {
|
||||
return connectionFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry configuration for operations such as validating topic creation
|
||||
* @param retryOperations the retry configuration
|
||||
*/
|
||||
public void setRetryOperations(RetryOperations retryOperations) {
|
||||
this.retryOperations = retryOperations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
// we instantiate the connection factory here due to https://jira.spring.io/browse/XD-2647
|
||||
ZookeeperConfiguration configuration = new ZookeeperConfiguration(this.zookeeperConnect);
|
||||
configuration.setBufferSize(socketBufferSize);
|
||||
configuration.setMaxWait(defaultMaxWait);
|
||||
DefaultConnectionFactory defaultConnectionFactory =
|
||||
new DefaultConnectionFactory(configuration);
|
||||
defaultConnectionFactory.afterPropertiesSet();
|
||||
this.connectionFactory = defaultConnectionFactory;
|
||||
if (retryOperations == null) {
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
|
||||
SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy();
|
||||
simpleRetryPolicy.setMaxAttempts(METADATA_VERIFICATION_RETRY_ATTEMPTS);
|
||||
retryTemplate.setRetryPolicy(simpleRetryPolicy);
|
||||
|
||||
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
|
||||
backOffPolicy.setInitialInterval(METADATA_VERIFICATION_RETRY_INITIAL_INTERVAL);
|
||||
backOffPolicy.setMultiplier(METADATA_VERIFICATION_RETRY_BACKOFF_MULTIPLIER);
|
||||
backOffPolicy.setMaxInterval(METADATA_VERIFICATION_MAX_INTERVAL);
|
||||
retryTemplate.setBackOffPolicy(backOffPolicy);
|
||||
retryOperations = retryTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allowed chars are ASCII alphanumerics, '.', '_' and '-'. '_' is used as escaped char in the form '_xx' where xx
|
||||
* is the hexadecimal value of the byte(s) needed to represent an illegal char in utf8.
|
||||
*/
|
||||
/*default*/
|
||||
public static String escapeTopicName(String original) {
|
||||
StringBuilder result = new StringBuilder(original.length());
|
||||
try {
|
||||
byte[] utf8 = original.getBytes("UTF-8");
|
||||
for (byte b : utf8) {
|
||||
if ((b >= 'a') && (b <= 'z') || (b >= 'A') && (b <= 'Z') || (b >= '0') && (b <= '9') || (b == '.')
|
||||
|| (b == '-')) {
|
||||
result.append((char) b);
|
||||
}
|
||||
else {
|
||||
result.append(String.format("_%02X", b));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError(e); // Can't happen
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public void setDefaultReplicationFactor(int defaultReplicationFactor) {
|
||||
this.defaultReplicationFactor = defaultReplicationFactor;
|
||||
}
|
||||
|
||||
public void setDefaultCompressionCodec(String defaultCompressionCodec) {
|
||||
this.defaultCompressionCodec = defaultCompressionCodec;
|
||||
}
|
||||
|
||||
public void setDefaultRequiredAcks(int defaultRequiredAcks) {
|
||||
this.defaultRequiredAcks = defaultRequiredAcks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default auto commit enabled property; This is used to commit the offset either automatically or
|
||||
* manually.
|
||||
* @param defaultAutoCommitEnabled
|
||||
*/
|
||||
public void setDefaultAutoCommitEnabled(boolean defaultAutoCommitEnabled) {
|
||||
this.defaultAutoCommitEnabled = defaultAutoCommitEnabled;
|
||||
}
|
||||
|
||||
public void setDefaultQueueSize(int defaultQueueSize) {
|
||||
this.defaultQueueSize = defaultQueueSize;
|
||||
}
|
||||
|
||||
public void setDefaultFetchSize(int defaultFetchSize) {
|
||||
this.defaultFetchSize = defaultFetchSize;
|
||||
}
|
||||
|
||||
public void setDefaultMinPartitionCount(int defaultMinPartitionCount) {
|
||||
this.defaultMinPartitionCount = defaultMinPartitionCount;
|
||||
}
|
||||
|
||||
public void setDefaultMaxWait(int defaultMaxWait) {
|
||||
this.defaultMaxWait = defaultMaxWait;
|
||||
}
|
||||
|
||||
public void setMode(Mode mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
@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, Properties properties) {
|
||||
// Usage of a different consumer group each time achieves pub-sub
|
||||
// PubSub consumers reset at the latest time, which allows them to receive only messages sent after
|
||||
// they've been bound
|
||||
String group = UUID.randomUUID().toString();
|
||||
createKafkaConsumer(name, inputChannel, properties, group, OffsetRequest.LatestTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void 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<Partition> partitions = ensureTopicCreated(topicName, numPartitions, defaultReplicationFactor);
|
||||
|
||||
ProducerMetadata<byte[], byte[]> 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<byte[], byte[]> producerFB =
|
||||
new ProducerFactoryBean<>(producerMetadata, brokers, additionalProps);
|
||||
|
||||
try {
|
||||
final ProducerConfiguration<byte[], byte[]> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindPubSubProducer(String name, MessageChannel outputChannel, Properties properties) {
|
||||
bindProducer(name, outputChannel, properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindRequestor(String name, MessageChannel requests, MessageChannel replies, Properties properties) {
|
||||
throw new UnsupportedOperationException("requestor binding is not supported by this bus");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindReplier(String name, MessageChannel requests, MessageChannel replies, Properties properties) {
|
||||
throw new UnsupportedOperationException("replier binding is not supported by this bus");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Kafka topic if needed, or try to increase its partition count to the desired number.
|
||||
*/
|
||||
private Collection<Partition> ensureTopicCreated(final String topicName, final int numPartitions,
|
||||
int replicationFactor) {
|
||||
|
||||
final int sessionTimeoutMs = 10000;
|
||||
final int connectionTimeoutMs = 10000;
|
||||
final ZkClient zkClient = new ZkClient(zkAddress, sessionTimeoutMs, connectionTimeoutMs, utf8Serializer);
|
||||
try {
|
||||
// The following is basically copy/paste from AdminUtils.createTopic() with
|
||||
// createOrUpdateTopicPartitionAssignmentPathInZK(..., update=true)
|
||||
final Properties topicConfig = new Properties();
|
||||
Seq<Object> brokerList = ZkUtils.getSortedBrokerList(zkClient);
|
||||
final scala.collection.Map<Object, Seq<Object>> replicaAssignment = AdminUtils.assignReplicasToBrokers
|
||||
(brokerList,
|
||||
numPartitions, replicationFactor, -1, -1);
|
||||
retryOperations.execute(new RetryCallback<Object, RuntimeException>() {
|
||||
|
||||
@Override
|
||||
public Object doWithRetry(RetryContext context) throws RuntimeException {
|
||||
AdminUtils.createOrUpdateTopicPartitionAssignmentPathInZK(zkClient, topicName, replicaAssignment,
|
||||
topicConfig, true);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
try {
|
||||
Collection<Partition> partitions = retryOperations.execute(new RetryCallback<Collection<Partition>, Exception>() {
|
||||
|
||||
@Override
|
||||
public Collection<Partition> doWithRetry(RetryContext context) throws Exception {
|
||||
connectionFactory.refreshMetadata(Collections.singleton(topicName));
|
||||
Collection<Partition> partitions = connectionFactory.getPartitions(topicName);
|
||||
if (partitions.size() < numPartitions) {
|
||||
throw new IllegalStateException("The number of expected partitions was: " + numPartitions
|
||||
+ ", but " +
|
||||
partitions.size() + " have been found instead");
|
||||
}
|
||||
connectionFactory.getLeaders(partitions);
|
||||
return partitions;
|
||||
}
|
||||
});
|
||||
return partitions;
|
||||
}
|
||||
catch (Exception e) {
|
||||
logger.error("Cannot initialize MessageBus", e);
|
||||
throw new RuntimeException("Cannot initialize message bus:", e);
|
||||
}
|
||||
|
||||
}
|
||||
finally {
|
||||
zkClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void 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);
|
||||
}
|
||||
KafkaPropertiesAccessor accessor = new KafkaPropertiesAccessor(properties);
|
||||
|
||||
int maxConcurrency = accessor.getConcurrency(defaultConcurrency);
|
||||
|
||||
String topic = escapeTopicName(name);
|
||||
|
||||
int numPartitions = accessor.getNumberOfKafkaPartitionsForConsumer();
|
||||
Collection<Partition> allPartitions = ensureTopicCreated(topic, numPartitions, defaultReplicationFactor);
|
||||
|
||||
Decoder<byte[]> valueDecoder = new DefaultDecoder(null);
|
||||
Decoder<byte[]> keyDecoder = new DefaultDecoder(null);
|
||||
|
||||
Collection<Partition> listenedPartitions;
|
||||
|
||||
int moduleCount = accessor.getCount();
|
||||
|
||||
if (moduleCount == 1) {
|
||||
listenedPartitions = allPartitions;
|
||||
}
|
||||
else {
|
||||
listenedPartitions = new ArrayList<Partition>();
|
||||
for (Partition partition : allPartitions) {
|
||||
// divide partitions across modules
|
||||
if (accessor.getPartitionIndex() != -1) {
|
||||
if ((partition.getId() % moduleCount) == accessor.getPartitionIndex()) {
|
||||
listenedPartitions.add(partition);
|
||||
}
|
||||
}
|
||||
else {
|
||||
int moduleSequence = accessor.getSequence();
|
||||
if (moduleCount == 0) {
|
||||
throw new IllegalArgumentException("The Kafka transport does not support 0-count modules");
|
||||
}
|
||||
else {
|
||||
// sequence numbers are zero-based
|
||||
if ((partition.getId() % moduleCount) == (moduleSequence - 1)) {
|
||||
listenedPartitions.add(partition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReceivingHandler rh = new ReceivingHandler();
|
||||
rh.setOutputChannel(moduleInputChannel);
|
||||
|
||||
final FixedSubscriberChannel bridge = new FixedSubscriberChannel(rh);
|
||||
bridge.setBeanName("bridge." + name);
|
||||
|
||||
final KafkaMessageListenerContainer messageListenerContainer =
|
||||
createMessageListenerContainer(accessor, group, maxConcurrency, listenedPartitions,
|
||||
referencePoint);
|
||||
|
||||
final KafkaMessageDrivenChannelAdapter kafkaMessageDrivenChannelAdapter =
|
||||
new KafkaMessageDrivenChannelAdapter(messageListenerContainer);
|
||||
kafkaMessageDrivenChannelAdapter.setBeanFactory(this.getBeanFactory());
|
||||
kafkaMessageDrivenChannelAdapter.setKeyDecoder(keyDecoder);
|
||||
kafkaMessageDrivenChannelAdapter.setPayloadDecoder(valueDecoder);
|
||||
kafkaMessageDrivenChannelAdapter.setOutputChannel(bridge);
|
||||
kafkaMessageDrivenChannelAdapter.setAutoCommitOffset(accessor.getDefaultAutoCommitEnabled(this
|
||||
.defaultAutoCommitEnabled));
|
||||
kafkaMessageDrivenChannelAdapter.afterPropertiesSet();
|
||||
kafkaMessageDrivenChannelAdapter.start();
|
||||
|
||||
|
||||
EventDrivenConsumer edc = new EventDrivenConsumer(bridge, rh) {
|
||||
|
||||
@Override
|
||||
protected void doStop() {
|
||||
// stop the offset manager and the channel adapter before unbinding
|
||||
// this means that the upstream channel adapter has a chance to stop
|
||||
kafkaMessageDrivenChannelAdapter.stop();
|
||||
if (messageListenerContainer.getOffsetManager() instanceof DisposableBean) {
|
||||
try {
|
||||
((DisposableBean) messageListenerContainer.getOffsetManager()).destroy();
|
||||
}
|
||||
catch (Exception e) {
|
||||
logger.error("Error while closing the offset manager", e);
|
||||
}
|
||||
}
|
||||
super.doStop();
|
||||
}
|
||||
};
|
||||
edc.setBeanName("inbound." + name);
|
||||
|
||||
Binding consumerBinding = Binding.forConsumer(name, edc, moduleInputChannel, accessor);
|
||||
addBinding(consumerBinding);
|
||||
consumerBinding.start();
|
||||
|
||||
}
|
||||
|
||||
public KafkaMessageListenerContainer createMessageListenerContainer(Properties properties, String group,
|
||||
int maxConcurrency, String topic, long referencePoint) {
|
||||
return createMessageListenerContainer(new KafkaPropertiesAccessor(properties), group, maxConcurrency, topic,
|
||||
null, referencePoint);
|
||||
}
|
||||
|
||||
private KafkaMessageListenerContainer createMessageListenerContainer(KafkaPropertiesAccessor accessor,
|
||||
String group, int maxConcurrency, Collection<Partition> listenedPartitions, long referencePoint) {
|
||||
return createMessageListenerContainer(accessor, group, maxConcurrency, null, listenedPartitions, referencePoint);
|
||||
}
|
||||
|
||||
private KafkaMessageListenerContainer createMessageListenerContainer(KafkaPropertiesAccessor accessor,
|
||||
String group, int maxConcurrency, String topic, Collection<Partition> listenedPartitions,
|
||||
long referencePoint) {
|
||||
Assert.isTrue(StringUtils.hasText(topic) ^ !CollectionUtils.isEmpty(listenedPartitions),
|
||||
"Exactly one of topic or a list of listened partitions must be provided");
|
||||
KafkaMessageListenerContainer messageListenerContainer;
|
||||
if (topic != null) {
|
||||
messageListenerContainer = new KafkaMessageListenerContainer(connectionFactory, topic);
|
||||
}
|
||||
else {
|
||||
messageListenerContainer = new KafkaMessageListenerContainer(connectionFactory,
|
||||
listenedPartitions.toArray(new Partition[listenedPartitions.size()]));
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Listening to topic " + topic);
|
||||
}
|
||||
// if we have less target partitions than target concurrency, adjust accordingly
|
||||
messageListenerContainer.setConcurrency(Math.min(maxConcurrency, listenedPartitions.size()));
|
||||
OffsetManager offsetManager = createOffsetManager(group, referencePoint);
|
||||
messageListenerContainer.setOffsetManager(offsetManager);
|
||||
messageListenerContainer.setQueueSize(accessor.getProperty(QUEUE_SIZE, defaultQueueSize));
|
||||
messageListenerContainer.setMaxFetch(accessor.getProperty(FETCH_SIZE, defaultFetchSize));
|
||||
return messageListenerContainer;
|
||||
}
|
||||
|
||||
private OffsetManager createOffsetManager(String group, long referencePoint) {
|
||||
try {
|
||||
KafkaTopicOffsetManager kafkaOffsetManager =
|
||||
new KafkaTopicOffsetManager(zookeeperConnect, offsetStoreTopic, Collections.<Partition,
|
||||
Long> emptyMap());
|
||||
kafkaOffsetManager.setConsumerId(group);
|
||||
kafkaOffsetManager.setReferenceTimestamp(referencePoint);
|
||||
kafkaOffsetManager.setSegmentSize(offsetStoreSegmentSize);
|
||||
kafkaOffsetManager.setRetentionTime(offsetStoreRetentionTime);
|
||||
kafkaOffsetManager.setRequiredAcks(offsetStoreRequiredAcks);
|
||||
kafkaOffsetManager.setMaxSize(offsetStoreMaxFetchSize);
|
||||
kafkaOffsetManager.setBatchBytes(offsetStoreBatchBytes);
|
||||
kafkaOffsetManager.setMaxQueueBufferingTime(offsetStoreBatchTime);
|
||||
|
||||
kafkaOffsetManager.afterPropertiesSet();
|
||||
|
||||
WindowingOffsetManager windowingOffsetManager = new WindowingOffsetManager(kafkaOffsetManager);
|
||||
windowingOffsetManager.setTimespan(offsetUpdateTimeWindow);
|
||||
windowingOffsetManager.setCount(offsetUpdateCount);
|
||||
windowingOffsetManager.setShutdownTimeout(offsetUpdateShutdownTimeout);
|
||||
|
||||
windowingOffsetManager.afterPropertiesSet();
|
||||
return windowingOffsetManager;
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doManualAck(LinkedList<MessageHeaders> messageHeadersList) {
|
||||
Iterator<MessageHeaders> iterator = messageHeadersList.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
MessageHeaders headers = iterator.next();
|
||||
Acknowledgment acknowledgment = (Acknowledgment) headers.get(KafkaHeaders.ACKNOWLEDGMENT);
|
||||
Assert.notNull(acknowledgment, "Acknowledgement shouldn't be null when acknowledging kafka message " +
|
||||
"manually.");
|
||||
acknowledgment.acknowledge();
|
||||
}
|
||||
}
|
||||
|
||||
private class KafkaPropertiesAccessor extends AbstractBusPropertiesAccessor {
|
||||
|
||||
public KafkaPropertiesAccessor(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
public int getNumberOfKafkaPartitionsForProducer() {
|
||||
int nextModuleCount = getNextModuleCount();
|
||||
if (nextModuleCount == 0) {
|
||||
throw new IllegalArgumentException("Module count cannot be zero");
|
||||
}
|
||||
int nextModuleConcurrency = getProperty(NEXT_MODULE_CONCURRENCY, defaultConcurrency);
|
||||
int minKafkaPartitions = getMinPartitionCount(defaultMinPartitionCount);
|
||||
return Math.max(minKafkaPartitions, nextModuleCount * nextModuleConcurrency);
|
||||
}
|
||||
|
||||
public int getNumberOfKafkaPartitionsForConsumer() {
|
||||
int concurrency = getConcurrency(defaultConcurrency);
|
||||
int minKafkaPartitions = getMinPartitionCount(defaultMinPartitionCount);
|
||||
int moduleCount = getCount();
|
||||
if (moduleCount == 0) {
|
||||
throw new IllegalArgumentException("Module count cannot be zero");
|
||||
}
|
||||
return Math.max(minKafkaPartitions, moduleCount * concurrency);
|
||||
}
|
||||
|
||||
public String getCompressionCodec(String defaultValue) {
|
||||
return getProperty(COMPRESSION_CODEC, defaultValue);
|
||||
}
|
||||
|
||||
public int getRequiredAcks(int defaultRequiredAcks) {
|
||||
return getProperty(REQUIRED_ACKS, defaultRequiredAcks);
|
||||
}
|
||||
|
||||
public boolean getDefaultAutoCommitEnabled(boolean defaultAutoCommitEnabled) {
|
||||
return getProperty(AUTO_COMMIT_ENABLED, defaultAutoCommitEnabled);
|
||||
}
|
||||
|
||||
public int getMinPartitionCount(int defaultPartitionCount) {
|
||||
return getProperty(MIN_PARTITION_COUNT, defaultPartitionCount);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class ReceivingHandler extends AbstractReplyProducingMessageHandler {
|
||||
|
||||
public ReceivingHandler() {
|
||||
this.setBeanFactory(KafkaMessageBus.this.getBeanFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Object handleRequestMessage(Message<?> requestMessage) {
|
||||
if (Mode.embeddedHeaders.equals(mode)) {
|
||||
MessageValues messageValues;
|
||||
try {
|
||||
messageValues = embeddedHeadersMessageConverter.extractHeaders((Message<byte[]>) requestMessage,
|
||||
true);
|
||||
}
|
||||
catch (Exception e) {
|
||||
logger.error(EmbeddedHeadersMessageConverter.decodeExceptionMessage(requestMessage), e);
|
||||
messageValues = new MessageValues(requestMessage);
|
||||
}
|
||||
messageValues = deserializePayloadIfNecessary(messageValues);
|
||||
return MessageBuilder.createMessage(messageValues.getPayload(), new KafkaBusMessageHeaders(
|
||||
messageValues));
|
||||
}
|
||||
else {
|
||||
return requestMessage;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private final class KafkaBusMessageHeaders extends MessageHeaders {
|
||||
|
||||
KafkaBusMessageHeaders(Map<String, Object> headers) {
|
||||
super(headers, MessageHeaders.ID_VALUE_NONE, -1L);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected boolean shouldCopyRequestHeaders() {
|
||||
// prevent the message from being copied again in superclass
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class SendingHandler extends AbstractMessageHandler {
|
||||
|
||||
private final PartitioningMetadata partitioningMetadata;
|
||||
|
||||
private final AtomicInteger roundRobinCount = new AtomicInteger();
|
||||
|
||||
private final String topicName;
|
||||
|
||||
private final int numberOfKafkaPartitions;
|
||||
|
||||
private final ProducerConfiguration<byte[], byte[]> producerConfiguration;
|
||||
|
||||
|
||||
private SendingHandler(String topicName, KafkaPropertiesAccessor properties, int numberOfPartitions,
|
||||
ProducerConfiguration<byte[], byte[]> producerConfiguration) {
|
||||
this.topicName = topicName;
|
||||
this.numberOfKafkaPartitions = numberOfPartitions;
|
||||
this.partitioningMetadata = new PartitioningMetadata(properties, numberOfPartitions);
|
||||
this.setBeanFactory(KafkaMessageBus.this.getBeanFactory());
|
||||
this.producerConfiguration = producerConfiguration;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleMessageInternal(Message<?> message) throws Exception {
|
||||
int targetPartition;
|
||||
if (partitioningMetadata.isPartitionedModule()) {
|
||||
targetPartition = determinePartition(message, partitioningMetadata);
|
||||
}
|
||||
else {
|
||||
targetPartition = roundRobin() % numberOfKafkaPartitions;
|
||||
}
|
||||
|
||||
if (Mode.embeddedHeaders.equals(mode)) {
|
||||
MessageValues transformed = serializePayloadIfNecessary(message);
|
||||
byte[] messageToSend = embeddedHeadersMessageConverter.embedHeaders(transformed,
|
||||
KafkaMessageBus.this.headersToMap);
|
||||
producerConfiguration.send(topicName, targetPartition, null, messageToSend);
|
||||
}
|
||||
else if (Mode.raw.equals(mode)) {
|
||||
Object contentType = message.getHeaders().get(MessageHeaders.CONTENT_TYPE);
|
||||
if (contentType != null
|
||||
&& !contentType.equals(MediaType.APPLICATION_OCTET_STREAM_VALUE)) {
|
||||
logger.error("Raw mode supports only " + MediaType.APPLICATION_OCTET_STREAM_VALUE + " content type"
|
||||
+ message.getPayload().getClass());
|
||||
}
|
||||
if (message.getPayload() instanceof byte[]) {
|
||||
producerConfiguration.send(topicName, targetPartition, null, (byte[]) message.getPayload());
|
||||
}
|
||||
else {
|
||||
logger.error("Raw mode supports only byte[] payloads but value sent was of type "
|
||||
+ message.getPayload().getClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int roundRobin() {
|
||||
int result = roundRobinCount.incrementAndGet();
|
||||
if (result == Integer.MAX_VALUE) {
|
||||
roundRobinCount.set(0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum Mode {
|
||||
raw,
|
||||
embeddedHeaders
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.kafka;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.integration.kafka.core.Partition;
|
||||
import org.springframework.integration.kafka.listener.OffsetManager;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import rx.Observable;
|
||||
import rx.Subscription;
|
||||
import rx.functions.Action0;
|
||||
import rx.functions.Action1;
|
||||
import rx.functions.Func1;
|
||||
import rx.functions.Func2;
|
||||
import rx.observables.GroupedObservable;
|
||||
import rx.observables.MathObservable;
|
||||
import rx.subjects.PublishSubject;
|
||||
import rx.subjects.SerializedSubject;
|
||||
import rx.subjects.Subject;
|
||||
|
||||
/**
|
||||
* An {@link OffsetManager} that aggregates writes over a time or count window, using an underlying delegate to
|
||||
* do the actual operations. Its purpose is to reduce the performance impact of writing operations
|
||||
* wherever this is desirable.
|
||||
*
|
||||
* Either a time window or a number of writes can be specified, but not both.
|
||||
*
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
//TODO: Move this class to spring-integration-kafka
|
||||
public class WindowingOffsetManager implements OffsetManager, InitializingBean, DisposableBean {
|
||||
|
||||
private final CreatePartitionAndOffsetFunction createPartitionAndOffsetFunction = new CreatePartitionAndOffsetFunction();
|
||||
|
||||
private final GetOffsetFunction getOffsetFunction = new GetOffsetFunction();
|
||||
|
||||
private final ComputeMaximumOffsetByPartitionFunction findHighestOffsetInPartitionGroup = new ComputeMaximumOffsetByPartitionFunction();
|
||||
|
||||
private final GetPartitionFunction getPartition = new GetPartitionFunction();
|
||||
|
||||
private final FindHighestOffsetsByPartitionFunction findHighestOffsetsByPartition = new FindHighestOffsetsByPartitionFunction();
|
||||
|
||||
private final DelegateUpdateOffsetAction delegateUpdateOffsetAction = new DelegateUpdateOffsetAction();
|
||||
|
||||
private final NotifyObservableClosedAction notifyObservableClosed = new NotifyObservableClosedAction();
|
||||
|
||||
private final OffsetManager delegate;
|
||||
|
||||
private long timespan = 10 * 1000;
|
||||
|
||||
private int count = 0;
|
||||
|
||||
private Subject<PartitionAndOffset, PartitionAndOffset> offsets;
|
||||
|
||||
private Subscription subscription;
|
||||
|
||||
private int shutdownTimeout = 2000;
|
||||
|
||||
private CountDownLatch shutdownLatch;
|
||||
|
||||
public WindowingOffsetManager(OffsetManager offsetManager) {
|
||||
this.delegate = offsetManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* The timespan for aggregating write operations, before invoking the underlying {@link OffsetManager}.
|
||||
*
|
||||
* @param timespan duration in milliseconds
|
||||
*/
|
||||
public void setTimespan(long timespan) {
|
||||
Assert.isTrue(timespan >= 0, "Timespan must be a positive value");
|
||||
this.timespan = timespan;
|
||||
}
|
||||
|
||||
/**
|
||||
* How many writes should be aggregated, before invoking the underlying {@link OffsetManager}. Setting this value
|
||||
* to 1 effectively disables windowing.
|
||||
*
|
||||
* @param count number of writes
|
||||
*/
|
||||
public void setCount(int count) {
|
||||
Assert.isTrue(count >= 0, "Count must be a positive value");
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
/**
|
||||
* The timeout that {@link #close()} and {@link #destroy()} operations will wait for receving a confirmation that the
|
||||
* underlying writes have been processed.
|
||||
*
|
||||
* @param shutdownTimeout duration in milliseconds
|
||||
*/
|
||||
public void setShutdownTimeout(int shutdownTimeout) {
|
||||
this.shutdownTimeout = shutdownTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
Assert.isTrue(timespan > 0 ^ count > 0, "Only one of the timespan or count must be set");
|
||||
// create the stream if windowing is set, and count is higher than 1
|
||||
if (timespan > 0 || count > 1) {
|
||||
offsets = new SerializedSubject<>(PublishSubject.<PartitionAndOffset>create());
|
||||
// window by either count or time
|
||||
Observable<Observable<PartitionAndOffset>> window =
|
||||
timespan > 0 ? offsets.window(timespan, TimeUnit.MILLISECONDS) : offsets.window(count);
|
||||
Observable<PartitionAndOffset> maximumOffsetsByWindow = window
|
||||
.flatMap(findHighestOffsetsByPartition)
|
||||
.doOnCompleted(notifyObservableClosed);
|
||||
subscription = maximumOffsetsByWindow.subscribe(delegateUpdateOffsetAction);
|
||||
}
|
||||
else {
|
||||
offsets = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
this.flush();
|
||||
this.close();
|
||||
if (delegate instanceof DisposableBean) {
|
||||
((DisposableBean) delegate).destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateOffset(Partition partition, long offset) {
|
||||
if (offsets != null) {
|
||||
offsets.onNext(new PartitionAndOffset(partition, offset));
|
||||
}
|
||||
else {
|
||||
delegate.updateOffset(partition, offset);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getOffset(Partition partition) {
|
||||
return delegate.getOffset(partition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteOffset(Partition partition) {
|
||||
delegate.deleteOffset(partition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetOffsets(Collection<Partition> partition) {
|
||||
delegate.resetOffsets(partition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (offsets != null) {
|
||||
shutdownLatch = new CountDownLatch(1);
|
||||
offsets.onCompleted();
|
||||
try {
|
||||
shutdownLatch.await(shutdownTimeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
delegate.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
delegate.flush();
|
||||
}
|
||||
|
||||
class PartitionAndOffset {
|
||||
|
||||
private final Partition partition;
|
||||
|
||||
private final Long offset;
|
||||
|
||||
public PartitionAndOffset(Partition partition, Long offset) {
|
||||
this.partition = partition;
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
public Partition getPartition() {
|
||||
return partition;
|
||||
}
|
||||
|
||||
public Long getOffset() {
|
||||
return offset;
|
||||
}
|
||||
}
|
||||
|
||||
private class DelegateUpdateOffsetAction implements Action1<PartitionAndOffset> {
|
||||
@Override
|
||||
public void call(PartitionAndOffset partitionAndOffset) {
|
||||
delegate.updateOffset(partitionAndOffset.getPartition(), partitionAndOffset.getOffset());
|
||||
}
|
||||
}
|
||||
|
||||
private class NotifyObservableClosedAction implements Action0 {
|
||||
@Override
|
||||
public void call() {
|
||||
if (shutdownLatch != null) {
|
||||
shutdownLatch.countDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CreatePartitionAndOffsetFunction implements Func2<Partition, Long, PartitionAndOffset> {
|
||||
@Override
|
||||
public PartitionAndOffset call(Partition partition, Long offset) {
|
||||
return new PartitionAndOffset(partition, offset);
|
||||
}
|
||||
}
|
||||
|
||||
private class GetOffsetFunction implements Func1<PartitionAndOffset, Long> {
|
||||
@Override
|
||||
public Long call(PartitionAndOffset partitionAndOffset) {
|
||||
return partitionAndOffset.getOffset();
|
||||
}
|
||||
}
|
||||
|
||||
private class ComputeMaximumOffsetByPartitionFunction implements Func1<GroupedObservable<Partition, PartitionAndOffset>, Observable<PartitionAndOffset>> {
|
||||
@Override
|
||||
public Observable<PartitionAndOffset> call(GroupedObservable<Partition, PartitionAndOffset> group) {
|
||||
return Observable.zip(Observable.just(group.getKey()),
|
||||
MathObservable.max(group.map(getOffsetFunction)),
|
||||
createPartitionAndOffsetFunction);
|
||||
}
|
||||
}
|
||||
|
||||
private class GetPartitionFunction implements Func1<PartitionAndOffset, Partition> {
|
||||
@Override
|
||||
public Partition call(PartitionAndOffset partitionAndOffset) {
|
||||
return partitionAndOffset.getPartition();
|
||||
}
|
||||
}
|
||||
|
||||
private class FindHighestOffsetsByPartitionFunction implements Func1<Observable<PartitionAndOffset>, Observable<PartitionAndOffset>> {
|
||||
@Override
|
||||
public Observable<PartitionAndOffset> call(Observable<PartitionAndOffset> windowBuffer) {
|
||||
return windowBuffer.groupBy(getPartition).flatMap(findHighestOffsetInPartitionGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:context="http://www.springframework.org/schema/context"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
|
||||
|
||||
|
||||
<bean id="zookeeperConnect" class="org.springframework.integration.kafka.support.ZookeeperConnect">
|
||||
<property name="zkConnect" value="${xd.messagebus.kafka.zkAddress}"/>
|
||||
</bean>
|
||||
|
||||
|
||||
<bean id="messageBus" class="org.springframework.xd.dirt.integration.kafka.KafkaMessageBus">
|
||||
<constructor-arg ref="zookeeperConnect"/>
|
||||
<constructor-arg value="${xd.messagebus.kafka.brokers}"/>
|
||||
<constructor-arg value="${xd.messagebus.kafka.zkAddress}"/>
|
||||
<constructor-arg ref="codec"/>
|
||||
<constructor-arg value="#{new String[0]}"/>
|
||||
|
||||
<property name="mode" value="${xd.messagebus.kafka.mode}"/>
|
||||
|
||||
<!-- Producer properties -->
|
||||
<property name="defaultBatchSize" value="${xd.messagebus.kafka.default.batchSize}"/>
|
||||
<property name="defaultBatchTimeout" value="${xd.messagebus.kafka.default.batchTimeout}"/>
|
||||
<property name="defaultRequiredAcks" value="${xd.messagebus.kafka.default.requiredAcks}"/>
|
||||
|
||||
<property name="defaultReplicationFactor" value="${xd.messagebus.kafka.default.replicationFactor}"/>
|
||||
<property name="defaultConcurrency" value="${xd.messagebus.kafka.default.concurrency}"/>
|
||||
<property name="defaultCompressionCodec" value="${xd.messagebus.kafka.default.compressionCodec}"/>
|
||||
<!-- ConsumerProperties -->
|
||||
<property name="defaultAutoCommitEnabled" value="${xd.messagebus.kafka.default.autoCommitEnabled}"/>
|
||||
<property name="defaultFetchSize" value="${xd.messagebus.kafka.default.fetchSize}"/>
|
||||
<property name="defaultMinPartitionCount" value="${xd.messagebus.kafka.default.minPartitionCount}"/>
|
||||
<property name="defaultQueueSize" value="${xd.messagebus.kafka.default.queueSize}"/>
|
||||
<!-- Offset Manager Properties-->
|
||||
<property name="offsetStoreTopic" value="${xd.messagebus.kafka.offsetStoreTopic}"/>
|
||||
<property name="offsetStoreSegmentSize" value="${xd.messagebus.kafka.offsetStoreSegmentSize}"/>
|
||||
<property name="offsetStoreRetentionTime" value="${xd.messagebus.kafka.offsetStoreRetentionTime}"/>
|
||||
<property name="offsetStoreRequiredAcks" value="${xd.messagebus.kafka.offsetStoreRequiredAcks}"/>
|
||||
<property name="offsetStoreMaxFetchSize" value="${xd.messagebus.kafka.offsetStoreMaxFetchSize}"/>
|
||||
<property name="offsetStoreBatchBytes" value="${xd.messagebus.kafka.offsetStoreBatchBytes}"/>
|
||||
<property name="offsetStoreBatchTime" value="${xd.messagebus.kafka.offsetStoreBatchTime}"/>
|
||||
<property name="offsetUpdateTimeWindow" value="${xd.messagebus.kafka.offsetUpdateTimeWindow}"/>
|
||||
<property name="offsetUpdateCount" value="${xd.messagebus.kafka.offsetUpdateCount}"/>
|
||||
<property name="offsetUpdateShutdownTimeout" value="${xd.messagebus.kafka.offsetUpdateShutdownTimeout}"/>
|
||||
</bean>
|
||||
|
||||
</beans>
|
||||
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* Copyright 2014-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus.kafka;
|
||||
|
||||
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import kafka.api.OffsetRequest;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.channel.QueueChannel;
|
||||
import org.springframework.integration.kafka.core.KafkaMessage;
|
||||
import org.springframework.integration.kafka.core.Partition;
|
||||
import org.springframework.integration.kafka.listener.KafkaMessageListenerContainer;
|
||||
import org.springframework.integration.kafka.listener.MessageListener;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.xd.dirt.integration.bus.BusProperties;
|
||||
import org.springframework.xd.dirt.integration.bus.MessageBus;
|
||||
import org.springframework.xd.dirt.integration.bus.PartitionCapableBusTests;
|
||||
import org.springframework.xd.dirt.integration.bus.Spy;
|
||||
import org.springframework.xd.dirt.integration.kafka.KafkaMessageBus;
|
||||
import org.springframework.xd.dirt.integration.kafka.KafkaTestSupport;
|
||||
|
||||
|
||||
/**
|
||||
* Integration tests for the {@link KafkaMessageBus}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
@Ignore //TODO: Fix this test
|
||||
public class KafkaMessageBusTests extends PartitionCapableBusTests {
|
||||
|
||||
@Rule
|
||||
public KafkaTestSupport kafkaTestSupport = new KafkaTestSupport();
|
||||
|
||||
private KafkaTestMessageBus messageBus;
|
||||
|
||||
@Override
|
||||
protected void busBindUnbindLatency() throws InterruptedException {
|
||||
Thread.sleep(500);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MessageBus getMessageBus() {
|
||||
if (messageBus == null) {
|
||||
messageBus = createKafkaTestMessageBus();
|
||||
}
|
||||
return messageBus;
|
||||
}
|
||||
|
||||
protected KafkaTestMessageBus createKafkaTestMessageBus() {
|
||||
return new KafkaTestMessageBus(kafkaTestSupport, getCodec(), KafkaMessageBus.Mode.embeddedHeaders);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean usesExplicitRouting() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Spy spyOn(final String name) {
|
||||
String topic = KafkaMessageBus.escapeTopicName(name);
|
||||
|
||||
KafkaTestMessageBus busWrapper = (KafkaTestMessageBus) getMessageBus();
|
||||
// Rewind offset, as tests will have typically already sent the messages we're trying to consume
|
||||
|
||||
KafkaMessageListenerContainer messageListenerContainer = busWrapper.getCoreMessageBus().createMessageListenerContainer(
|
||||
new Properties(), UUID.randomUUID().toString(), 1, topic, OffsetRequest.EarliestTime());
|
||||
|
||||
final BlockingQueue<KafkaMessage> messages = new ArrayBlockingQueue<KafkaMessage>(10);
|
||||
|
||||
messageListenerContainer.setMessageListener(new MessageListener() {
|
||||
|
||||
@Override
|
||||
public void onMessage(KafkaMessage message) {
|
||||
messages.offer(message);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return new Spy() {
|
||||
|
||||
@Override
|
||||
public Object receive(boolean expectNull) throws Exception {
|
||||
return messages.poll(expectNull ? 50 : 5000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompression() throws Exception {
|
||||
final String[] codecs = new String[] { null, "none", "gzip", "snappy" };
|
||||
|
||||
byte[] ratherBigPayload = new byte[2048];
|
||||
Arrays.fill(ratherBigPayload, (byte) 65);
|
||||
MessageBus messageBus = getMessageBus();
|
||||
|
||||
for (String codec : codecs) {
|
||||
DirectChannel moduleOutputChannel = new DirectChannel();
|
||||
QueueChannel moduleInputChannel = new QueueChannel();
|
||||
Properties props = new Properties();
|
||||
if (codec != null) {
|
||||
props.put(KafkaMessageBus.COMPRESSION_CODEC, codec);
|
||||
}
|
||||
messageBus.bindProducer("foo.0", moduleOutputChannel, props);
|
||||
messageBus.bindConsumer("foo.0", moduleInputChannel, null);
|
||||
Message<?> message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build();
|
||||
// Let the consumer actually bind to the producer before sending a msg
|
||||
busBindUnbindLatency();
|
||||
moduleOutputChannel.send(message);
|
||||
Message<?> inbound = moduleInputChannel.receive(2000);
|
||||
assertNotNull(inbound);
|
||||
assertArrayEquals(ratherBigPayload, (byte[]) inbound.getPayload());
|
||||
messageBus.unbindProducers("foo.0");
|
||||
messageBus.unbindConsumers("foo.0");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomPartitionCountOverridesDefaultIfLarger() throws Exception {
|
||||
|
||||
byte[] ratherBigPayload = new byte[2048];
|
||||
Arrays.fill(ratherBigPayload, (byte) 65);
|
||||
KafkaTestMessageBus messageBus = (KafkaTestMessageBus) getMessageBus();
|
||||
|
||||
|
||||
DirectChannel moduleOutputChannel = new DirectChannel();
|
||||
QueueChannel moduleInputChannel = new QueueChannel();
|
||||
Properties producerProperties = new Properties();
|
||||
producerProperties.put(BusProperties.MIN_PARTITION_COUNT, "10");
|
||||
Properties consumerProperties = new Properties();
|
||||
consumerProperties.put(BusProperties.MIN_PARTITION_COUNT, "10");
|
||||
long uniqueBindingId = System.currentTimeMillis();
|
||||
messageBus.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties);
|
||||
messageBus.bindConsumer("foo" + uniqueBindingId + ".0", moduleInputChannel, consumerProperties);
|
||||
Message<?> message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build();
|
||||
// Let the consumer actually bind to the producer before sending a msg
|
||||
busBindUnbindLatency();
|
||||
moduleOutputChannel.send(message);
|
||||
Message<?> inbound = moduleInputChannel.receive(2000);
|
||||
assertNotNull(inbound);
|
||||
assertArrayEquals(ratherBigPayload, (byte[]) inbound.getPayload());
|
||||
Collection<Partition> partitions = messageBus.getCoreMessageBus().getConnectionFactory().getPartitions(
|
||||
"foo" + uniqueBindingId + ".0");
|
||||
assertThat(partitions, hasSize(10));
|
||||
messageBus.unbindProducers("foo" + uniqueBindingId + ".0");
|
||||
messageBus.unbindConsumers("foo" + uniqueBindingId + ".0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomPartitionCountDoesNotOverrideModuleCountAndConcurrencyIfSmaller() throws Exception {
|
||||
|
||||
byte[] ratherBigPayload = new byte[2048];
|
||||
Arrays.fill(ratherBigPayload, (byte) 65);
|
||||
KafkaTestMessageBus messageBus = (KafkaTestMessageBus) getMessageBus();
|
||||
|
||||
|
||||
DirectChannel moduleOutputChannel = new DirectChannel();
|
||||
QueueChannel moduleInputChannel = new QueueChannel();
|
||||
Properties producerProps = new Properties();
|
||||
producerProps.put(BusProperties.MIN_PARTITION_COUNT, "5");
|
||||
producerProps.put(BusProperties.NEXT_MODULE_CONCURRENCY, "6");
|
||||
Properties consumerProps = new Properties();
|
||||
consumerProps.put(BusProperties.MIN_PARTITION_COUNT, "5");
|
||||
consumerProps.put(BusProperties.CONCURRENCY, "6");
|
||||
long uniqueBindingId = System.currentTimeMillis();
|
||||
messageBus.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProps);
|
||||
messageBus.bindConsumer("foo" + uniqueBindingId + ".0", moduleInputChannel, consumerProps);
|
||||
Message<?> message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build();
|
||||
// Let the consumer actually bind to the producer before sending a msg
|
||||
busBindUnbindLatency();
|
||||
moduleOutputChannel.send(message);
|
||||
Message<?> inbound = moduleInputChannel.receive(2000);
|
||||
assertNotNull(inbound);
|
||||
assertArrayEquals(ratherBigPayload, (byte[]) inbound.getPayload());
|
||||
Collection<Partition> partitions = messageBus.getCoreMessageBus().getConnectionFactory().getPartitions(
|
||||
"foo" + uniqueBindingId + ".0");
|
||||
assertThat(partitions, hasSize(6));
|
||||
messageBus.unbindProducers("foo" + uniqueBindingId + ".0");
|
||||
messageBus.unbindConsumers("foo" + uniqueBindingId + ".0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomPartitionCountOverridesModuleCountAndConcurrencyIfLarger() throws Exception {
|
||||
|
||||
byte[] ratherBigPayload = new byte[2048];
|
||||
Arrays.fill(ratherBigPayload, (byte) 65);
|
||||
KafkaTestMessageBus messageBus = (KafkaTestMessageBus) getMessageBus();
|
||||
|
||||
DirectChannel moduleOutputChannel = new DirectChannel();
|
||||
QueueChannel moduleInputChannel = new QueueChannel();
|
||||
Properties producerProps = new Properties();
|
||||
producerProps.put(BusProperties.MIN_PARTITION_COUNT, "6");
|
||||
producerProps.put(BusProperties.NEXT_MODULE_CONCURRENCY, "5");
|
||||
Properties consumerProps = new Properties();
|
||||
consumerProps.put(BusProperties.MIN_PARTITION_COUNT, "6");
|
||||
consumerProps.put(BusProperties.CONCURRENCY, "5");
|
||||
long uniqueBindingId = System.currentTimeMillis();
|
||||
messageBus.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProps);
|
||||
messageBus.bindConsumer("foo" + uniqueBindingId + ".0", moduleInputChannel, consumerProps);
|
||||
Message<?> message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build();
|
||||
// Let the consumer actually bind to the producer before sending a msg
|
||||
busBindUnbindLatency();
|
||||
moduleOutputChannel.send(message);
|
||||
Message<?> inbound = moduleInputChannel.receive(2000);
|
||||
assertNotNull(inbound);
|
||||
assertArrayEquals(ratherBigPayload, (byte[]) inbound.getPayload());
|
||||
Collection<Partition> partitions = messageBus.getCoreMessageBus().getConnectionFactory().getPartitions(
|
||||
"foo" + uniqueBindingId + ".0");
|
||||
assertThat(partitions, hasSize(6));
|
||||
messageBus.unbindProducers("foo" + uniqueBindingId + ".0");
|
||||
messageBus.unbindConsumers("foo" + uniqueBindingId + ".0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomPartitionCountDoesNotOverridePartitioningIfSmaller() throws Exception {
|
||||
|
||||
byte[] ratherBigPayload = new byte[2048];
|
||||
Arrays.fill(ratherBigPayload, (byte) 65);
|
||||
KafkaTestMessageBus messageBus = (KafkaTestMessageBus) getMessageBus();
|
||||
|
||||
DirectChannel moduleOutputChannel = new DirectChannel();
|
||||
QueueChannel moduleInputChannel = new QueueChannel();
|
||||
Properties producerProperties = new Properties();
|
||||
producerProperties.put(BusProperties.MIN_PARTITION_COUNT, "3");
|
||||
producerProperties.put(BusProperties.NEXT_MODULE_COUNT, "5");
|
||||
producerProperties.put(BusProperties.PARTITION_KEY_EXPRESSION, "payload");
|
||||
Properties consumerProperties = new Properties();
|
||||
consumerProperties.put(BusProperties.MIN_PARTITION_COUNT, "3");
|
||||
long uniqueBindingId = System.currentTimeMillis();
|
||||
messageBus.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties);
|
||||
messageBus.bindConsumer("foo" + uniqueBindingId + ".0", moduleInputChannel, consumerProperties);
|
||||
Message<?> message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build();
|
||||
// Let the consumer actually bind to the producer before sending a msg
|
||||
busBindUnbindLatency();
|
||||
moduleOutputChannel.send(message);
|
||||
Message<?> inbound = moduleInputChannel.receive(2000);
|
||||
assertNotNull(inbound);
|
||||
assertArrayEquals(ratherBigPayload, (byte[]) inbound.getPayload());
|
||||
Collection<Partition> partitions = messageBus.getCoreMessageBus().getConnectionFactory().getPartitions(
|
||||
"foo" + uniqueBindingId + ".0");
|
||||
assertThat(partitions, hasSize(5));
|
||||
messageBus.unbindProducers("foo" + uniqueBindingId + ".0");
|
||||
messageBus.unbindConsumers("foo" + uniqueBindingId + ".0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomPartitionCountOverridesPartitioningIfLarger() throws Exception {
|
||||
|
||||
byte[] ratherBigPayload = new byte[2048];
|
||||
Arrays.fill(ratherBigPayload, (byte) 65);
|
||||
KafkaTestMessageBus messageBus = (KafkaTestMessageBus) getMessageBus();
|
||||
|
||||
DirectChannel moduleOutputChannel = new DirectChannel();
|
||||
QueueChannel moduleInputChannel = new QueueChannel();
|
||||
Properties producerProperties = new Properties();
|
||||
producerProperties.put(BusProperties.MIN_PARTITION_COUNT, "5");
|
||||
producerProperties.put(BusProperties.NEXT_MODULE_COUNT, "3");
|
||||
producerProperties.put(BusProperties.PARTITION_KEY_EXPRESSION, "payload");
|
||||
Properties consumerProperties = new Properties();
|
||||
consumerProperties.put(BusProperties.MIN_PARTITION_COUNT, "5");
|
||||
long uniqueBindingId = System.currentTimeMillis();
|
||||
messageBus.bindProducer("foo" + uniqueBindingId + ".0", moduleOutputChannel, producerProperties);
|
||||
messageBus.bindConsumer("foo" + uniqueBindingId + ".0", moduleInputChannel, consumerProperties);
|
||||
Message<?> message = org.springframework.integration.support.MessageBuilder.withPayload(ratherBigPayload).build();
|
||||
// Let the consumer actually bind to the producer before sending a msg
|
||||
busBindUnbindLatency();
|
||||
moduleOutputChannel.send(message);
|
||||
Message<?> inbound = moduleInputChannel.receive(2000);
|
||||
assertNotNull(inbound);
|
||||
assertArrayEquals(ratherBigPayload, (byte[]) inbound.getPayload());
|
||||
Collection<Partition> partitions = messageBus.getCoreMessageBus().getConnectionFactory().getPartitions(
|
||||
"foo" + uniqueBindingId + ".0");
|
||||
assertThat(partitions, hasSize(5));
|
||||
messageBus.unbindProducers("foo" + uniqueBindingId + ".0");
|
||||
messageBus.unbindConsumers("foo" + uniqueBindingId + ".0");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Kafka message bus does not support direct binding")
|
||||
@Override
|
||||
public void testDirectBinding() throws Exception {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus.kafka;
|
||||
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.integration.kafka.support.ZookeeperConnect;
|
||||
import org.springframework.xd.dirt.integration.bus.AbstractTestMessageBus;
|
||||
import org.springframework.xd.dirt.integration.bus.serializer.MultiTypeCodec;
|
||||
import org.springframework.xd.dirt.integration.bus.serializer.kryo.PojoCodec;
|
||||
import org.springframework.xd.dirt.integration.kafka.KafkaMessageBus;
|
||||
import org.springframework.xd.dirt.integration.kafka.TestKafkaCluster;
|
||||
import org.springframework.xd.dirt.integration.kafka.KafkaTestSupport;
|
||||
import org.springframework.xd.tuple.serializer.kryo.TupleKryoRegistrar;
|
||||
|
||||
|
||||
/**
|
||||
* Test support class for {@link KafkaMessageBus}.
|
||||
* Creates a bus that uses a test {@link TestKafkaCluster kafka cluster}.
|
||||
* @author Eric Bottard
|
||||
* @author Marius Bogoevici
|
||||
* @author David Turanski
|
||||
*/
|
||||
public class KafkaTestMessageBus extends AbstractTestMessageBus<KafkaMessageBus> {
|
||||
|
||||
public KafkaTestMessageBus(KafkaTestSupport kafkaTestSupport) {
|
||||
this(kafkaTestSupport, getCodec(), KafkaMessageBus.Mode.embeddedHeaders);
|
||||
}
|
||||
|
||||
|
||||
public KafkaTestMessageBus(KafkaTestSupport kafkaTestSupport, MultiTypeCodec<Object> codec,
|
||||
KafkaMessageBus.Mode mode) {
|
||||
|
||||
try {
|
||||
ZookeeperConnect zookeeperConnect = new ZookeeperConnect();
|
||||
zookeeperConnect.setZkConnect(kafkaTestSupport.getZkConnectString());
|
||||
KafkaMessageBus messageBus = new KafkaMessageBus(zookeeperConnect,
|
||||
kafkaTestSupport.getBrokerAddress(),
|
||||
kafkaTestSupport.getZkConnectString(), codec);
|
||||
messageBus.setDefaultBatchingEnabled(false);
|
||||
messageBus.setMode(mode);
|
||||
messageBus.afterPropertiesSet();
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
context.refresh();
|
||||
messageBus.setApplicationContext(context);
|
||||
this.setMessageBus(messageBus);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// do nothing - the rule will take care of that
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private static MultiTypeCodec<Object> getCodec() {
|
||||
return new PojoCodec(new TupleKryoRegistrar());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus.kafka;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.xd.dirt.integration.bus.PartitionKeyExtractorStrategy;
|
||||
import org.springframework.xd.dirt.integration.bus.PartitionSelectorStrategy;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
public class RawKafkaPartitionTestSupport implements PartitionKeyExtractorStrategy, PartitionSelectorStrategy {
|
||||
|
||||
@Override
|
||||
public int selectPartition(Object key, int divisor) {
|
||||
return ((byte[])key)[0] % divisor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object extractKey(Message<?> message) {
|
||||
return message.getPayload();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus.kafka;
|
||||
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
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.MessageHeaders;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.xd.dirt.integration.bus.Binding;
|
||||
import org.springframework.xd.dirt.integration.bus.BusProperties;
|
||||
import org.springframework.xd.dirt.integration.bus.MessageBus;
|
||||
import org.springframework.xd.dirt.integration.bus.XdHeaders;
|
||||
import org.springframework.xd.dirt.integration.kafka.KafkaMessageBus;
|
||||
import org.springframework.xd.test.TestUtils;
|
||||
|
||||
/**
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
@Ignore //TODO: Fix this test
|
||||
public class RawModeKafkaMessageBusTests extends KafkaMessageBusTests {
|
||||
|
||||
@Override
|
||||
protected KafkaTestMessageBus createKafkaTestMessageBus() {
|
||||
return new KafkaTestMessageBus(kafkaTestSupport, getCodec(), KafkaMessageBus.Mode.raw);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Override
|
||||
public void testPartitionedModuleJava() throws Exception {
|
||||
MessageBus bus = getMessageBus();
|
||||
Properties properties = new Properties();
|
||||
properties.put("partitionKeyExtractorClass", "org.springframework.xd.dirt.integration.bus.kafka.RawKafkaPartitionTestSupport");
|
||||
properties.put("partitionSelectorClass", "org.springframework.xd.dirt.integration.bus.kafka.RawKafkaPartitionTestSupport");
|
||||
properties.put(BusProperties.NEXT_MODULE_COUNT, "3");
|
||||
properties.put(BusProperties.NEXT_MODULE_CONCURRENCY, "2");
|
||||
|
||||
DirectChannel output = new DirectChannel();
|
||||
output.setBeanName("test.output");
|
||||
bus.bindProducer("partJ.0", output, properties);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.bindings", List.class);
|
||||
assertEquals(1, bindings.size());
|
||||
|
||||
properties.clear();
|
||||
properties.put("concurrency", "2");
|
||||
properties.put("count","3");
|
||||
properties.put("partitionIndex", "0");
|
||||
QueueChannel input0 = new QueueChannel();
|
||||
input0.setBeanName("test.input0J");
|
||||
bus.bindConsumer("partJ.0", input0, properties);
|
||||
properties.put("partitionIndex", "1");
|
||||
QueueChannel input1 = new QueueChannel();
|
||||
input1.setBeanName("test.input1J");
|
||||
bus.bindConsumer("partJ.0", input1, properties);
|
||||
properties.put("partitionIndex", "2");
|
||||
QueueChannel input2 = new QueueChannel();
|
||||
input2.setBeanName("test.input2J");
|
||||
bus.bindConsumer("partJ.0", input2, properties);
|
||||
|
||||
output.send(new GenericMessage<>(new byte[]{(byte)0}));
|
||||
output.send(new GenericMessage<>(new byte[]{(byte)1}));
|
||||
output.send(new GenericMessage<>(new byte[]{(byte)2}));
|
||||
|
||||
Message<?> receive0 = input0.receive(1000);
|
||||
assertNotNull(receive0);
|
||||
Message<?> receive1 = input1.receive(1000);
|
||||
assertNotNull(receive1);
|
||||
Message<?> receive2 = input2.receive(1000);
|
||||
assertNotNull(receive2);
|
||||
|
||||
assertThat(Arrays.asList(
|
||||
((byte[]) receive0.getPayload())[0],
|
||||
((byte[]) receive1.getPayload())[0],
|
||||
((byte[]) receive2.getPayload())[0]),
|
||||
containsInAnyOrder((byte)0, (byte)1, (byte)2));
|
||||
|
||||
bus.unbindConsumers("partJ.0");
|
||||
bus.unbindProducers("partJ.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Override
|
||||
public void testPartitionedModuleSpEL() throws Exception {
|
||||
MessageBus bus = getMessageBus();
|
||||
Properties properties = new Properties();
|
||||
properties.put("partitionKeyExpression", "payload[0]");
|
||||
properties.put("partitionSelectorExpression", "hashCode()");
|
||||
properties.put(BusProperties.NEXT_MODULE_COUNT, "3");
|
||||
properties.put(BusProperties.NEXT_MODULE_CONCURRENCY, "2");
|
||||
|
||||
DirectChannel output = new DirectChannel();
|
||||
output.setBeanName("test.output");
|
||||
bus.bindProducer("part.0", output, properties);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.bindings", List.class);
|
||||
assertEquals(1, bindings.size());
|
||||
try {
|
||||
AbstractEndpoint endpoint = bindings.get(0).getEndpoint();
|
||||
assertThat(getEndpointRouting(endpoint), containsString("part.0-' + headers['partition']"));
|
||||
}
|
||||
catch (UnsupportedOperationException ignored) {
|
||||
|
||||
}
|
||||
|
||||
properties.clear();
|
||||
properties.put("concurrency", "2");
|
||||
properties.put("partitionIndex", "0");
|
||||
properties.put("count","3");
|
||||
QueueChannel input0 = new QueueChannel();
|
||||
input0.setBeanName("test.input0S");
|
||||
bus.bindConsumer("part.0", input0, properties);
|
||||
properties.put("partitionIndex", "1");
|
||||
QueueChannel input1 = new QueueChannel();
|
||||
input1.setBeanName("test.input1S");
|
||||
bus.bindConsumer("part.0", input1, properties);
|
||||
properties.put("partitionIndex", "2");
|
||||
QueueChannel input2 = new QueueChannel();
|
||||
input2.setBeanName("test.input2S");
|
||||
bus.bindConsumer("part.0", input2, properties);
|
||||
|
||||
Message<byte[]> message2 = MessageBuilder.withPayload(new byte[]{2})
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, "foo")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, 42)
|
||||
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_SIZE, 43)
|
||||
.setHeader("xdReplyChannel", "bar")
|
||||
.build();
|
||||
output.send(message2);
|
||||
output.send(new GenericMessage<>(new byte[]{1}));
|
||||
output.send(new GenericMessage<>(new byte[]{0}));
|
||||
|
||||
Message<?> receive0 = input0.receive(1000);
|
||||
assertNotNull(receive0);
|
||||
Message<?> receive1 = input1.receive(1000);
|
||||
assertNotNull(receive1);
|
||||
Message<?> receive2 = input2.receive(1000);
|
||||
assertNotNull(receive2);
|
||||
|
||||
|
||||
assertThat(Arrays.asList(
|
||||
((byte[]) receive0.getPayload())[0],
|
||||
((byte[]) receive1.getPayload())[0],
|
||||
((byte[]) receive2.getPayload())[0]),
|
||||
containsInAnyOrder((byte)0, (byte)1, (byte)2));
|
||||
|
||||
bus.unbindConsumers("part.0");
|
||||
bus.unbindProducers("part.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Override
|
||||
public void createInboundPubSubBeforeOutboundPubSub() throws Exception {
|
||||
MessageBus messageBus = getMessageBus();
|
||||
DirectChannel moduleOutputChannel = new DirectChannel();
|
||||
// Test pub/sub by emulating how StreamPlugin handles taps
|
||||
DirectChannel tapChannel = new DirectChannel();
|
||||
QueueChannel moduleInputChannel = new QueueChannel();
|
||||
QueueChannel module2InputChannel = new QueueChannel();
|
||||
QueueChannel module3InputChannel = new QueueChannel();
|
||||
// Create the tap first
|
||||
String fooTapName = messageBus.isCapable(MessageBus.Capability.DURABLE_PUBSUB) ? "foo.tap:baz.http" : "tap:baz.http";
|
||||
messageBus.bindPubSubConsumer(fooTapName, module2InputChannel, null);
|
||||
|
||||
// Then create the stream
|
||||
messageBus.bindProducer("baz.0", moduleOutputChannel, null);
|
||||
messageBus.bindConsumer("baz.0", moduleInputChannel, null);
|
||||
moduleOutputChannel.addInterceptor(new WireTap(tapChannel));
|
||||
messageBus.bindPubSubProducer("tap:baz.http", tapChannel, null);
|
||||
|
||||
// Another new module is using tap as an input channel
|
||||
String barTapName = messageBus.isCapable(MessageBus.Capability.DURABLE_PUBSUB) ? "bar.tap:baz.http" : "tap:baz.http";
|
||||
messageBus.bindPubSubConsumer(barTapName, module3InputChannel, null);
|
||||
Message<?> message = MessageBuilder.withPayload("foo".getBytes()).setHeader(MessageHeaders.CONTENT_TYPE, "foo/bar").build();
|
||||
boolean success = false;
|
||||
boolean retried = false;
|
||||
while (!success) {
|
||||
moduleOutputChannel.send(message);
|
||||
Message<?> inbound = moduleInputChannel.receive(5000);
|
||||
assertNotNull(inbound);
|
||||
assertEquals("foo", new String((byte[])inbound.getPayload()));
|
||||
Message<?> tapped1 = module2InputChannel.receive(5000);
|
||||
Message<?> tapped2 = module3InputChannel.receive(5000);
|
||||
if (tapped1 == null || tapped2 == null) {
|
||||
// listener may not have started
|
||||
assertFalse("Failed to receive tap after retry", retried);
|
||||
retried = true;
|
||||
continue;
|
||||
}
|
||||
success = true;
|
||||
assertEquals("foo", new String((byte[]) tapped1.getPayload()));
|
||||
assertNull(tapped1.getHeaders().get(XdHeaders.XD_ORIGINAL_CONTENT_TYPE));
|
||||
assertEquals("foo", new String((byte[])tapped2.getPayload()));
|
||||
}
|
||||
// delete one tap stream is deleted
|
||||
messageBus.unbindConsumer(barTapName, module3InputChannel);
|
||||
Message<?> message2 = MessageBuilder.withPayload("bar".getBytes()).setHeader(MessageHeaders.CONTENT_TYPE, "foo/bar").build();
|
||||
moduleOutputChannel.send(message2);
|
||||
|
||||
// other tap still receives messages
|
||||
Message<?> tapped = module2InputChannel.receive(5000);
|
||||
assertNotNull(tapped);
|
||||
|
||||
// Removed tap does not
|
||||
assertNull(module3InputChannel.receive(1000));
|
||||
|
||||
// when other tap stream is deleted
|
||||
messageBus.unbindConsumer(fooTapName, module2InputChannel);
|
||||
// Clean up as StreamPlugin would
|
||||
messageBus.unbindConsumer("baz.0", moduleInputChannel);
|
||||
messageBus.unbindProducer("baz.0", moduleOutputChannel);
|
||||
messageBus.unbindProducers("tap:baz.http");
|
||||
assertTrue(getBindings(messageBus).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Override
|
||||
public void testSendAndReceive() throws Exception {
|
||||
MessageBus messageBus = getMessageBus();
|
||||
DirectChannel moduleOutputChannel = new DirectChannel();
|
||||
QueueChannel moduleInputChannel = new QueueChannel();
|
||||
messageBus.bindProducer("foo.0", moduleOutputChannel, null);
|
||||
messageBus.bindConsumer("foo.0", moduleInputChannel, null);
|
||||
Message<?> message = MessageBuilder.withPayload("foo".getBytes()).build();
|
||||
// Let the consumer actually bind to the producer before sending a msg
|
||||
busBindUnbindLatency();
|
||||
moduleOutputChannel.send(message);
|
||||
Message<?> inbound = moduleInputChannel.receive(5000);
|
||||
assertNotNull(inbound);
|
||||
assertEquals("foo", new String((byte[])inbound.getPayload()));
|
||||
messageBus.unbindProducers("foo.0");
|
||||
messageBus.unbindConsumers("foo.0");
|
||||
}
|
||||
|
||||
// Ignored, since raw mode does not support headers
|
||||
@Test
|
||||
@Override
|
||||
@Ignore
|
||||
public void testSendAndReceiveNoOriginalContentType() throws Exception {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendAndReceivePubSub() throws Exception {
|
||||
MessageBus messageBus = getMessageBus();
|
||||
DirectChannel moduleOutputChannel = new DirectChannel();
|
||||
// Test pub/sub by emulating how StreamPlugin handles taps
|
||||
DirectChannel tapChannel = new DirectChannel();
|
||||
QueueChannel moduleInputChannel = new QueueChannel();
|
||||
QueueChannel module2InputChannel = new QueueChannel();
|
||||
QueueChannel module3InputChannel = new QueueChannel();
|
||||
messageBus.bindProducer("baz.0", moduleOutputChannel, null);
|
||||
messageBus.bindConsumer("baz.0", moduleInputChannel, null);
|
||||
moduleOutputChannel.addInterceptor(new WireTap(tapChannel));
|
||||
messageBus.bindPubSubProducer("tap:baz.http", tapChannel, null);
|
||||
// A new module is using the tap as an input channel
|
||||
String fooTapName = messageBus.isCapable(MessageBus.Capability.DURABLE_PUBSUB) ? "foo.tap:baz.http" : "tap:baz.http";
|
||||
messageBus.bindPubSubConsumer(fooTapName, module2InputChannel, null);
|
||||
// Another new module is using tap as an input channel
|
||||
String barTapName = messageBus.isCapable(MessageBus.Capability.DURABLE_PUBSUB) ? "bar.tap:baz.http" : "tap:baz.http";
|
||||
messageBus.bindPubSubConsumer(barTapName, module3InputChannel, null);
|
||||
Message<?> message = MessageBuilder.withPayload("foo".getBytes()).build();
|
||||
boolean success = false;
|
||||
boolean retried = false;
|
||||
while (!success) {
|
||||
moduleOutputChannel.send(message);
|
||||
Message<?> inbound = moduleInputChannel.receive(5000);
|
||||
assertNotNull(inbound);
|
||||
assertEquals("foo", new String((byte[])inbound.getPayload()));
|
||||
|
||||
Message<?> tapped1 = module2InputChannel.receive(5000);
|
||||
Message<?> tapped2 = module3InputChannel.receive(5000);
|
||||
if (tapped1 == null || tapped2 == null) {
|
||||
// listener may not have started
|
||||
assertFalse("Failed to receive tap after retry", retried);
|
||||
retried = true;
|
||||
continue;
|
||||
}
|
||||
success = true;
|
||||
assertEquals("foo", new String((byte[])tapped1.getPayload()));
|
||||
assertEquals("foo", new String((byte[])tapped2.getPayload()));
|
||||
}
|
||||
// delete one tap stream is deleted
|
||||
messageBus.unbindConsumer(barTapName, module3InputChannel);
|
||||
Message<?> message2 = MessageBuilder.withPayload("bar".getBytes()).build();
|
||||
moduleOutputChannel.send(message2);
|
||||
|
||||
// other tap still receives messages
|
||||
Message<?> tapped = module2InputChannel.receive(5000);
|
||||
assertNotNull(tapped);
|
||||
|
||||
// Removed tap does not
|
||||
assertNull(module3InputChannel.receive(1000));
|
||||
|
||||
// when other tap stream is deleted
|
||||
messageBus.unbindConsumer(fooTapName, module2InputChannel);
|
||||
// Clean up as StreamPlugin would
|
||||
messageBus.unbindConsumer("baz.0", moduleInputChannel);
|
||||
messageBus.unbindProducer("baz.0", moduleOutputChannel);
|
||||
messageBus.unbindProducers("tap:baz.http");
|
||||
assertTrue(getBindings(messageBus).isEmpty());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.springframework.xd.dirt.integration.kafka;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import kafka.utils.TestUtils$;
|
||||
import kafka.utils.Utils$;
|
||||
import org.apache.zookeeper.server.NIOServerCnxnFactory;
|
||||
import org.apache.zookeeper.server.ZooKeeperServer;
|
||||
|
||||
/**
|
||||
* A port of kafka.zk.EmbeddedZookeeper, compatible with Zookeeper 3.4 API
|
||||
*
|
||||
* @author Marius Bogoevici
|
||||
*/
|
||||
public class EmbeddedZookeeper {
|
||||
|
||||
private String connectString;
|
||||
|
||||
private File snapshotDir = TestUtils$.MODULE$.tempDir();
|
||||
|
||||
private File logDir = TestUtils$.MODULE$.tempDir();
|
||||
|
||||
private int tickTime = 500;
|
||||
|
||||
private final ZooKeeperServer zookeeper;
|
||||
|
||||
private int port;
|
||||
|
||||
private final NIOServerCnxnFactory factory;
|
||||
|
||||
public EmbeddedZookeeper(String connectString) throws Exception {
|
||||
this.connectString = connectString;
|
||||
port = Integer.parseInt(connectString.split(":")[1]);
|
||||
zookeeper = new ZooKeeperServer(snapshotDir, logDir, tickTime);
|
||||
factory = new NIOServerCnxnFactory();
|
||||
factory.configure(new InetSocketAddress("127.0.0.1", port), 100);
|
||||
factory.startup(zookeeper);
|
||||
}
|
||||
|
||||
public String getConnectString() {
|
||||
return connectString;
|
||||
}
|
||||
|
||||
public File getSnapshotDir() {
|
||||
return snapshotDir;
|
||||
}
|
||||
|
||||
public File getLogDir() {
|
||||
return logDir;
|
||||
}
|
||||
|
||||
public int getTickTime() {
|
||||
return tickTime;
|
||||
}
|
||||
|
||||
public ZooKeeperServer getZookeeper() {
|
||||
return zookeeper;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
try {
|
||||
zookeeper.shutdown();
|
||||
}
|
||||
catch (Exception e) {
|
||||
// ignore exception
|
||||
}
|
||||
try {
|
||||
factory.shutdown();
|
||||
}
|
||||
catch (Exception e) {
|
||||
// ignore exception
|
||||
}
|
||||
try {
|
||||
Utils$.MODULE$.rm(logDir);
|
||||
}
|
||||
catch (Exception e) {
|
||||
// ignore exception
|
||||
}
|
||||
try {
|
||||
Utils$.MODULE$.rm(snapshotDir);
|
||||
}
|
||||
catch (Exception e) {
|
||||
// ignore exception
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2014-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.kafka;
|
||||
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import kafka.server.KafkaConfig;
|
||||
import kafka.server.KafkaServer;
|
||||
import kafka.utils.SystemTime$;
|
||||
import kafka.utils.TestUtils;
|
||||
import kafka.utils.TestZKUtils;
|
||||
import kafka.utils.Utils;
|
||||
import kafka.utils.ZKStringSerializer$;
|
||||
import kafka.utils.ZkUtils;
|
||||
import org.I0Itec.zkclient.ZkClient;
|
||||
import org.I0Itec.zkclient.exception.ZkInterruptedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.junit.Rule;
|
||||
|
||||
import org.springframework.xd.test.AbstractExternalResourceTestSupport;
|
||||
|
||||
/**
|
||||
* JUnit {@link Rule} that starts an embedded Kafka server (with an associated Zookeeper)
|
||||
*
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @author Marius Bogoevici
|
||||
* @since 1.1
|
||||
*/
|
||||
public class KafkaTestSupport extends AbstractExternalResourceTestSupport<String> {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(KafkaTestSupport.class);
|
||||
|
||||
private static final String XD_KAFKA_TEST_EMBEDDED = "XD_KAFKA_TEST_EMBEDDED";
|
||||
|
||||
public static final boolean embedded;
|
||||
|
||||
private static final String DEFAULT_ZOOKEEPER_CONNECT = "localhost:2181";
|
||||
|
||||
private static final String DEFAULT_KAFKA_CONNECT = "localhost:9092";
|
||||
|
||||
private ZkClient zkClient;
|
||||
|
||||
private EmbeddedZookeeper zookeeper;
|
||||
|
||||
private KafkaServer kafkaServer;
|
||||
|
||||
private Properties brokerConfig = TestUtils.createBrokerConfig(0, TestUtils.choosePort(), false);
|
||||
|
||||
static {
|
||||
embedded = "true".equals(System.getProperty(XD_KAFKA_TEST_EMBEDDED));
|
||||
log.info(String.format("Testing with %s Kafka broker", embedded ? "embedded" : "external"));
|
||||
}
|
||||
|
||||
public KafkaTestSupport() {
|
||||
super("KAFKA");
|
||||
}
|
||||
|
||||
public String getZkConnectString() {
|
||||
if (embedded) {
|
||||
return zookeeper.getConnectString();
|
||||
}
|
||||
else {
|
||||
return DEFAULT_ZOOKEEPER_CONNECT;
|
||||
}
|
||||
}
|
||||
|
||||
public ZkClient getZkClient() {
|
||||
return this.zkClient;
|
||||
}
|
||||
|
||||
public String getBrokerAddress() {
|
||||
if (embedded) {
|
||||
return kafkaServer.config().hostName() + ":" + kafkaServer.config().port();
|
||||
}
|
||||
else {
|
||||
return DEFAULT_KAFKA_CONNECT;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void obtainResource() throws Exception {
|
||||
if (embedded) {
|
||||
log.debug("Starting Zookeeper");
|
||||
zookeeper = new EmbeddedZookeeper(TestZKUtils.zookeeperConnect());
|
||||
log.debug("Started Zookeeper at " + zookeeper.getConnectString());
|
||||
try {
|
||||
int zkConnectionTimeout = 6000;
|
||||
int zkSessionTimeout = 6000;
|
||||
zkClient = new ZkClient(getZkConnectString(), zkSessionTimeout, zkConnectionTimeout, ZKStringSerializer$.MODULE$);
|
||||
}
|
||||
catch (Exception e) {
|
||||
zookeeper.shutdown();
|
||||
throw e;
|
||||
}
|
||||
try {
|
||||
log.debug("Creating Kafka server");
|
||||
Properties brokerConfigProperties = brokerConfig;
|
||||
kafkaServer = TestUtils.createServer(new KafkaConfig(brokerConfigProperties), SystemTime$.MODULE$);
|
||||
log.debug("Created Kafka server at " + kafkaServer.config().hostName() + ":" + kafkaServer.config().port());
|
||||
}
|
||||
catch (Exception e) {
|
||||
zookeeper.shutdown();
|
||||
zkClient.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.zkClient = new ZkClient(DEFAULT_ZOOKEEPER_CONNECT, 5000, 5000, ZKStringSerializer$.MODULE$);
|
||||
if (ZkUtils.getAllBrokersInCluster(zkClient).size() == 0) {
|
||||
throw new RuntimeException("Kafka server not available");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanupResource() throws Exception {
|
||||
if (embedded) {
|
||||
try {
|
||||
kafkaServer.shutdown();
|
||||
}
|
||||
catch (Exception e) {
|
||||
// ignore errors on shutdown
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
try {
|
||||
Utils.rm(kafkaServer.config().logDirs());
|
||||
}
|
||||
catch (Exception e) {
|
||||
// ignore errors on shutdown
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
zkClient.close();
|
||||
}
|
||||
catch (ZkInterruptedException e) {
|
||||
// ignore errors on shutdown
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
if (embedded) {
|
||||
try {
|
||||
zookeeper.shutdown();
|
||||
}
|
||||
catch (Exception e) {
|
||||
// ignore errors on shutdown
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.kafka;
|
||||
|
||||
import kafka.admin.AdminUtils;
|
||||
import kafka.consumer.Consumer;
|
||||
import kafka.consumer.ConsumerConfig;
|
||||
import kafka.javaapi.consumer.ConsumerConnector;
|
||||
import kafka.server.KafkaConfig;
|
||||
import kafka.server.KafkaServerStartable;
|
||||
|
||||
import kafka.utils.TestUtils;
|
||||
import org.I0Itec.zkclient.ZkClient;
|
||||
import org.apache.curator.framework.CuratorFramework;
|
||||
import org.apache.curator.framework.CuratorFrameworkFactory;
|
||||
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
|
||||
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
|
||||
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
|
||||
import org.apache.curator.retry.RetryUntilElapsed;
|
||||
import org.apache.curator.test.TestingServer;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.SocketUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Properties;
|
||||
|
||||
|
||||
/**
|
||||
* A test Kafka + ZooKeeper pair for testing purposes.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class TestKafkaCluster {
|
||||
|
||||
private KafkaServerStartable kafkaServer;
|
||||
|
||||
private TestingServer zkServer;
|
||||
|
||||
public TestKafkaCluster() {
|
||||
try {
|
||||
zkServer = new TestingServer(SocketUtils.findAvailableTcpPort());
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
KafkaConfig config = getKafkaConfig(zkServer.getConnectString());
|
||||
kafkaServer = new KafkaServerStartable(config);
|
||||
kafkaServer.startup();
|
||||
}
|
||||
|
||||
private static KafkaConfig getKafkaConfig(final String zkConnectString) {
|
||||
scala.collection.Iterator<Properties> propsI =
|
||||
TestUtils.createBrokerConfigs(1, false).iterator();
|
||||
assert propsI.hasNext();
|
||||
Properties props = propsI.next();
|
||||
assert props.containsKey("zookeeper.connect");
|
||||
props.put("zookeeper.connect", zkConnectString);
|
||||
return new KafkaConfig(props);
|
||||
}
|
||||
|
||||
public String getKafkaBrokerString() {
|
||||
return String.format("localhost:%d",
|
||||
kafkaServer.serverConfig().port());
|
||||
}
|
||||
|
||||
public void stop() throws IOException {
|
||||
kafkaServer.shutdown();
|
||||
zkServer.stop();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* See XD-2293. This is used to reproduce Kafka rebalance issues.
|
||||
*/
|
||||
public static void main(String[] args) throws Exception {
|
||||
TestKafkaCluster cluster = new TestKafkaCluster();
|
||||
ZkClient client = new ZkClient(cluster.getZkConnectString(), 10000, 10000, KafkaMessageBus.utf8Serializer);
|
||||
int partitions = 5;
|
||||
int replication = 1;
|
||||
AdminUtils.createTopic(client, "mytopic", partitions, replication, new Properties());
|
||||
|
||||
Properties props = new Properties();
|
||||
props.put("zookeeper.connect", cluster.getZkConnectString());
|
||||
props.put("group.id", "foo");
|
||||
props.put("rebalance.backoff.ms", "2000");
|
||||
props.put("rebalance.max.retries", "2000");
|
||||
ConsumerConfig config = new ConsumerConfig(props);
|
||||
|
||||
|
||||
CuratorFramework curator = CuratorFrameworkFactory.newClient(cluster.getZkConnectString(), new RetryUntilElapsed(1000, 100));
|
||||
curator.start();
|
||||
|
||||
RebalanceListener listener = null;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
System.out.format("%nCreating consumer #%d%n", i + 1);
|
||||
ConsumerConnector connector = Consumer.createJavaConsumerConnector(config);
|
||||
connector.createMessageStreams(Collections.singletonMap("mytopic", 1));
|
||||
if (i == 0) {
|
||||
PathChildrenCache cache = new PathChildrenCache(curator, "/consumers/foo/owners/mytopic", true);
|
||||
listener = new RebalanceListener(5);
|
||||
cache.getListenable().addListener(listener);
|
||||
cache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
|
||||
}
|
||||
|
||||
synchronized (listener) {
|
||||
System.out.println("******** Waiting for rebalance...");
|
||||
listener.wait();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
|
||||
}
|
||||
|
||||
public String getZkConnectString() {
|
||||
return zkServer.getConnectString();
|
||||
}
|
||||
|
||||
private static class RebalanceListener implements PathChildrenCacheListener {
|
||||
|
||||
private int expected;
|
||||
|
||||
private int actual;
|
||||
|
||||
private boolean ready;
|
||||
|
||||
public RebalanceListener(int expected) {
|
||||
this.expected = expected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
|
||||
System.out.println(event);
|
||||
System.out.println(event.getData() != null ? new String(event.getData().getData()) : "no data");
|
||||
switch (event.getType()) {
|
||||
case CHILD_ADDED:
|
||||
actual++;
|
||||
if (ready && actual == expected) {
|
||||
System.out.println("*** Moving on... ");
|
||||
this.notify();
|
||||
}
|
||||
break;
|
||||
case CHILD_REMOVED:
|
||||
actual--;
|
||||
break;
|
||||
case INITIALIZED:
|
||||
Assert.isTrue(actual == expected);
|
||||
ready = true;
|
||||
this.notify();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-streams-binding-local</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>spring-cloud-streams-binding-local</name>
|
||||
<description>Local(in memory) binding implementation</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-bindings-parent</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-spi</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-test</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,418 @@
|
||||
/*
|
||||
* Copyright 2013-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus.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.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;
|
||||
import org.springframework.integration.handler.BridgeHandler;
|
||||
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;
|
||||
import org.springframework.xd.dirt.integration.bus.AbstractBusPropertiesAccessor;
|
||||
import org.springframework.xd.dirt.integration.bus.Binding;
|
||||
import org.springframework.xd.dirt.integration.bus.BusProperties;
|
||||
import org.springframework.xd.dirt.integration.bus.MessageBusSupport;
|
||||
|
||||
/**
|
||||
* A simple implementation of {@link org.springframework.xd.dirt.integration.bus.MessageBus} for in-process use. For inbound and outbound, creates a
|
||||
* {@link DirectChannel} or a {@link QueueChannel} depending on whether the binding is aliased or not then bridges the
|
||||
* passed {@link MessageChannel} to the channel which is registered in the given application context. If that channel
|
||||
* does not yet exist, it will be created.
|
||||
*
|
||||
* @author David Turanski
|
||||
* @author Mark Fisher
|
||||
* @author Gary Russell
|
||||
* @author Jennifer Hickey
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @since 1.0
|
||||
*/
|
||||
public class LocalMessageBus extends MessageBusSupport {
|
||||
|
||||
private static final int DEFAULT_EXECUTOR_CORE_POOL_SIZE = 0;
|
||||
|
||||
private static final int DEFAULT_EXECUTOR_MAX_POOL_SIZE = 200;
|
||||
|
||||
private static final int DEFAULT_EXECUTOR_QUEUE_SIZE = Integer.MAX_VALUE;
|
||||
|
||||
private static final int DEFAULT_EXECUTOR_KEEPALIVE_SECONDS = 60;
|
||||
|
||||
private static final int DEFAULT_REQ_REPLY_CONCURRENCY = 1;
|
||||
|
||||
protected static final Set<Object> CONSUMER_REQUEST_REPLY_PROPERTIES = new SetBuilder()
|
||||
.addAll(CONSUMER_STANDARD_PROPERTIES)
|
||||
.add(BusProperties.CONCURRENCY)
|
||||
.build();
|
||||
|
||||
private volatile PollerMetadata poller;
|
||||
|
||||
private final Map<String, ExecutorChannel> requestReplyChannels = new HashMap<String, ExecutorChannel>();
|
||||
|
||||
private final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
|
||||
private volatile int executorCorePoolSize = DEFAULT_EXECUTOR_CORE_POOL_SIZE;
|
||||
|
||||
private volatile int executorMaxPoolSize = DEFAULT_EXECUTOR_MAX_POOL_SIZE;
|
||||
|
||||
private volatile int executorQueueSize = DEFAULT_EXECUTOR_QUEUE_SIZE;
|
||||
|
||||
private volatile int executorKeepAliveSeconds = DEFAULT_EXECUTOR_KEEPALIVE_SECONDS;
|
||||
|
||||
private volatile int queueSize = Integer.MAX_VALUE;
|
||||
|
||||
private final Map<String, ThreadPoolTaskExecutor> reqRepExecutors = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Used to create and customize {@link QueueChannel}s when the binding operation involves aliased names.
|
||||
*/
|
||||
private final SharedChannelProvider<QueueChannel> queueChannelProvider = new SharedChannelProvider<QueueChannel>(
|
||||
QueueChannel.class) {
|
||||
|
||||
@Override
|
||||
protected QueueChannel createSharedChannel(String name) {
|
||||
QueueChannel queueChannel = new QueueChannel(queueSize);
|
||||
return queueChannel;
|
||||
}
|
||||
};
|
||||
|
||||
private final SharedChannelProvider<PublishSubscribeChannel> pubsubChannelProvider = new SharedChannelProvider<PublishSubscribeChannel>(
|
||||
PublishSubscribeChannel.class) {
|
||||
|
||||
@Override
|
||||
protected PublishSubscribeChannel createSharedChannel(String name) {
|
||||
PublishSubscribeChannel publishSubscribeChannel = new PublishSubscribeChannel(executor);
|
||||
publishSubscribeChannel.setIgnoreFailures(true);
|
||||
return publishSubscribeChannel;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the poller to use when QueueChannels are used.
|
||||
*/
|
||||
public void setPoller(PollerMetadata poller) {
|
||||
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.
|
||||
* Default: 0 (threads created on demand until maxPoolSize).
|
||||
* @param executorCorePoolSize the pool size.
|
||||
*/
|
||||
public void setExecutorCorePoolSize(int executorCorePoolSize) {
|
||||
this.executorCorePoolSize = executorCorePoolSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link ThreadPoolTaskExecutor}} max pool size to limit the number of concurrent
|
||||
* threads. The executor is used for PubSub operations.
|
||||
* Default: 200.
|
||||
* @param executorMaxPoolSize the pool size.
|
||||
*/
|
||||
public void setExecutorMaxPoolSize(int executorMaxPoolSize) {
|
||||
this.executorMaxPoolSize = executorMaxPoolSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link ThreadPoolTaskExecutor}} queue size to limit the number of concurrent
|
||||
* threads. The executor is used for PubSub operations.
|
||||
* Default: {@link Integer#MAX_VALUE}.
|
||||
* @param executorQueueSize the queue size.
|
||||
*/
|
||||
public void setExecutorQueueSize(int executorQueueSize) {
|
||||
this.executorQueueSize = executorQueueSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link ThreadPoolTaskExecutor}} keep alive seconds.
|
||||
* The executor is used for PubSub operations.
|
||||
* @param executorKeepAliveSeconds the keep alive seconds.
|
||||
*/
|
||||
public void setExecutorKeepAliveSeconds(int executorKeepAliveSeconds) {
|
||||
this.executorKeepAliveSeconds = executorKeepAliveSeconds;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onInit() {
|
||||
this.executor.setCorePoolSize(this.executorCorePoolSize);
|
||||
this.executor.setMaxPoolSize(this.executorMaxPoolSize);
|
||||
this.executor.setQueueCapacity(this.executorQueueSize);
|
||||
this.executor.setKeepAliveSeconds(this.executorKeepAliveSeconds);
|
||||
this.executor.setThreadNamePrefix("xd.localbus-");
|
||||
this.executor.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* For the local bus 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 bus 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, Properties properties) {
|
||||
validateConsumerProperties(name, properties, CONSUMER_STANDARD_PROPERTIES);
|
||||
doRegisterConsumer(name, moduleInputChannel, this.pubsubChannelProvider, properties);
|
||||
}
|
||||
|
||||
private void 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);
|
||||
bridge(name, registeredChannel, moduleInputChannel,
|
||||
"inbound." + ((NamedComponent) registeredChannel).getComponentName(),
|
||||
new LocalBusPropertiesAccessor(properties));
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up or creates a DirectChannel with the given name and creates a bridge to that channel from the provided
|
||||
* channel instance.
|
||||
*/
|
||||
@Override
|
||||
public void bindProducer(String name, MessageChannel moduleOutputChannel, Properties properties) {
|
||||
validateConsumerProperties(name, properties, PRODUCER_STANDARD_PROPERTIES);
|
||||
doRegisterProducer(name, moduleOutputChannel, getChannelProvider(name), 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,
|
||||
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);
|
||||
bridge(name, moduleOutputChannel, registeredChannel,
|
||||
"outbound." + ((NamedComponent) registeredChannel).getComponentName(),
|
||||
new LocalBusPropertiesAccessor(properties));
|
||||
}
|
||||
|
||||
@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);
|
||||
// TODO: handle Pollable ?
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: handle Pollable ?
|
||||
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 LocalBusPropertiesAccessor(properties).getConcurrency(DEFAULT_REQ_REPLY_CONCURRENCY));
|
||||
executor.setThreadNamePrefix("xd.localBus." + 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();
|
||||
}
|
||||
}
|
||||
|
||||
protected BridgeHandler bridge(String name, MessageChannel from, MessageChannel to, String bridgeName,
|
||||
LocalBusPropertiesAccessor properties) {
|
||||
return bridge(name, from, to, bridgeName, null, properties);
|
||||
}
|
||||
|
||||
|
||||
protected BridgeHandler bridge(String name, MessageChannel from, MessageChannel to, String bridgeName,
|
||||
final Collection<MimeType> acceptedMimeTypes, LocalBusPropertiesAccessor properties) {
|
||||
|
||||
final boolean isInbound = bridgeName.startsWith("inbound.");
|
||||
|
||||
BridgeHandler handler = new BridgeHandler() {
|
||||
|
||||
@Override
|
||||
protected boolean shouldCopyRequestHeaders() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handleRequestMessage(Message<?> requestMessage) {
|
||||
return requestMessage;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
handler.setBeanFactory(getBeanFactory());
|
||||
handler.setOutputChannel(to);
|
||||
handler.setBeanName(bridgeName);
|
||||
handler.afterPropertiesSet();
|
||||
|
||||
// Usage of a CEFB allows to handle both Subscribable & Pollable channels the same way
|
||||
ConsumerEndpointFactoryBean cefb = new ConsumerEndpointFactoryBean();
|
||||
cefb.setInputChannel(from);
|
||||
cefb.setHandler(handler);
|
||||
cefb.setBeanFactory(getBeanFactory());
|
||||
if (from instanceof PollableChannel) {
|
||||
cefb.setPollerMetadata(poller);
|
||||
}
|
||||
try {
|
||||
cefb.afterPropertiesSet();
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
cefb.getObject().setComponentName(handler.getComponentName());
|
||||
Binding binding = isInbound ? Binding.forConsumer(name, cefb.getObject(), to, properties)
|
||||
: Binding.forProducer(name, from, cefb.getObject(), properties);
|
||||
addBinding(binding);
|
||||
binding.start();
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
protected <T> T getBean(String name, Class<T> requiredType) {
|
||||
return getApplicationContext().getBean(name, requiredType);
|
||||
}
|
||||
|
||||
private static class LocalBusPropertiesAccessor extends AbstractBusPropertiesAccessor {
|
||||
|
||||
public LocalBusPropertiesAccessor(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:int="http://www.springframework.org/schema/integration"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
|
||||
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<bean id="messageBus" class="org.springframework.xd.dirt.integration.bus.local.LocalMessageBus">
|
||||
<property name="queueSize"
|
||||
value="${xd.messagebus.local.queueSize: #{T(Integer).MAX_VALUE}}" />
|
||||
<property name="poller">
|
||||
<int:poller fixed-rate="${xd.messagebus.local.polling}" />
|
||||
</property>
|
||||
<property name="executorCorePoolSize" value="${xd.messagebus.local.executor.corePoolSize}" />
|
||||
<property name="executorMaxPoolSize" value="${xd.messagebus.local.executor.maxPoolSize}" />
|
||||
<property name="executorQueueSize" value="${xd.messagebus.local.executor.queueSize: #{T(Integer).MAX_VALUE}}" />
|
||||
<property name="executorKeepAliveSeconds" value="${xd.messagebus.local.executor.keepAliveSeconds}" />
|
||||
</bean>
|
||||
</beans>
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright 2013-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus.local;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.channel.interceptor.WireTap;
|
||||
import org.springframework.integration.support.DefaultMessageBuilderFactory;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.integration.support.utils.IntegrationUtils;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.xd.dirt.integration.bus.AbstractMessageBusTests;
|
||||
import org.springframework.xd.dirt.integration.bus.MessageBus;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @author David Turanski
|
||||
* @since 1.0
|
||||
*/
|
||||
public class LocalMessageBusTests extends AbstractMessageBusTests {
|
||||
|
||||
@Override
|
||||
protected MessageBus getMessageBus() throws Exception {
|
||||
LocalMessageBus bus = new LocalMessageBus();
|
||||
GenericApplicationContext applicationContext = new GenericApplicationContext();
|
||||
applicationContext.getBeanFactory().registerSingleton(
|
||||
IntegrationUtils.INTEGRATION_MESSAGE_BUILDER_FACTORY_BEAN_NAME,
|
||||
new DefaultMessageBuilderFactory());
|
||||
applicationContext.refresh();
|
||||
bus.setApplicationContext(applicationContext);
|
||||
bus.setExecutorCorePoolSize(2);
|
||||
bus.setExecutorMaxPoolSize(10);
|
||||
bus.setExecutorKeepAliveSeconds(59);
|
||||
bus.setExecutorQueueSize(Integer.MAX_VALUE - 1);
|
||||
bus.afterPropertiesSet();
|
||||
return bus;
|
||||
}
|
||||
|
||||
protected Collection<?> getBindings(MessageBus testMessageBus) {
|
||||
return getBindingsFromMsgBus(testMessageBus);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProps() throws Exception {
|
||||
LocalMessageBus bus = (LocalMessageBus) getMessageBus();
|
||||
ThreadPoolTaskExecutor exec = TestUtils.getPropertyValue(bus, "executor", ThreadPoolTaskExecutor.class);
|
||||
assertEquals(2, exec.getCorePoolSize());
|
||||
assertEquals(10, exec.getMaxPoolSize());
|
||||
assertEquals(59, exec.getKeepAliveSeconds());
|
||||
Assert.assertEquals(Integer.MAX_VALUE - 1, TestUtils.getPropertyValue(exec, "queueCapacity"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPayloadConversionNotNeededExplicitType() throws Exception {
|
||||
LocalMessageBus bus = (LocalMessageBus) getMessageBus();
|
||||
verifyPayloadConversion(new TestPayload(), bus);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoPayloadConversionByDefault() throws Exception {
|
||||
LocalMessageBus bus = (LocalMessageBus) getMessageBus();
|
||||
verifyPayloadConversion(new TestPayload(), bus);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTapDoesntHurtStream() throws Exception {
|
||||
LocalMessageBus bus = (LocalMessageBus) getMessageBus();
|
||||
DirectChannel moduleOutputChannel = new DirectChannel();
|
||||
moduleOutputChannel.setBeanName("bangOut");
|
||||
DirectChannel tapChannel = new DirectChannel();
|
||||
tapChannel.setBeanName("tapChannel");
|
||||
WireTap tap = new WireTap(tapChannel);
|
||||
moduleOutputChannel.addInterceptor(tap);
|
||||
bus.bindProducer("bang.0", moduleOutputChannel, null);
|
||||
final AtomicBoolean messageReceived = new AtomicBoolean();
|
||||
final AtomicReference<Thread> streamThread = new AtomicReference<Thread>();
|
||||
bus.bindConsumer("bang.0", new DirectChannel() {
|
||||
|
||||
@Override
|
||||
protected boolean doSend(Message<?> message, long timeout) {
|
||||
messageReceived.set(true);
|
||||
streamThread.set(Thread.currentThread());
|
||||
return true;
|
||||
}
|
||||
}, null);
|
||||
final CountDownLatch tapped = new CountDownLatch(1);
|
||||
final AtomicReference<Thread> tapThread = new AtomicReference<Thread>();
|
||||
bus.bindPubSubProducer("tap:stream:bang.0", tapChannel, null);
|
||||
bus.bindPubSubConsumer("tap:stream:bang.0", new DirectChannel() {
|
||||
|
||||
@Override
|
||||
protected boolean doSend(Message<?> message, long timeout) {
|
||||
tapThread.set(Thread.currentThread());
|
||||
tapped.countDown();
|
||||
throw new RuntimeException("bang");
|
||||
}
|
||||
}, null);
|
||||
moduleOutputChannel.send(new GenericMessage<String>("Foo"));
|
||||
assertTrue(tapped.await(10, TimeUnit.SECONDS));
|
||||
assertTrue(messageReceived.get());
|
||||
assertSame(Thread.currentThread(), streamThread.get());
|
||||
assertNotNull(tapThread.get());
|
||||
assertNotSame(Thread.currentThread(), tapThread.get());
|
||||
}
|
||||
|
||||
private void verifyPayloadConversion(final Object expectedValue, final LocalMessageBus bus) {
|
||||
DirectChannel myChannel = new DirectChannel();
|
||||
bus.bindConsumer("in", myChannel, null);
|
||||
DirectChannel input = bus.getBean("in", DirectChannel.class);
|
||||
assertNotNull(input);
|
||||
|
||||
final AtomicBoolean msgSent = new AtomicBoolean(false);
|
||||
|
||||
myChannel.subscribe(new MessageHandler() {
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message<?> message) throws MessagingException {
|
||||
assertEquals(expectedValue, message.getPayload());
|
||||
msgSent.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
Message<TestPayload> msg = MessageBuilder.withPayload(new TestPayload())
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE, MediaType.ALL_VALUE).build();
|
||||
|
||||
input.send(msg);
|
||||
assertTrue(msgSent.get());
|
||||
}
|
||||
|
||||
static class TestPayload {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "foo";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return (other instanceof TestPayload && this.toString().equals(other.toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.toString().hashCode();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-streams-binding-rabbit</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>spring-cloud-streams-binding-rabbit</name>
|
||||
<description>RabbitMQ binding implementation</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-bindings-parent</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-spi</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-test</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-amqp</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.integration</groupId>
|
||||
<artifactId>spring-integration-amqp</artifactId>
|
||||
<version>${spring-integration.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2011-2015 the original author or authors.
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.rabbit;
|
||||
|
||||
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.amqp.RabbitProperties;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
/**
|
||||
* Configures the connection factory used by the rabbit message bus.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Gary Russell
|
||||
*/
|
||||
@Configuration
|
||||
public class ConnectionFactorySettings {
|
||||
|
||||
@Value("${spring.rabbitmq.useSSL:false}")
|
||||
private boolean useSSL;
|
||||
|
||||
@Value("${spring.rabbitmq.sslProperties:}")
|
||||
private Resource sslPropertiesLocation;
|
||||
|
||||
@Bean
|
||||
// TODO: Move to spring boot
|
||||
public ConnectionFactory rabbitConnectionFactory(RabbitProperties config,
|
||||
com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory) throws Exception {
|
||||
CachingConnectionFactory factory = new CachingConnectionFactory(rabbitConnectionFactory);
|
||||
factory.setAddresses(config.getAddresses());
|
||||
if (config.getHost() != null) {
|
||||
factory.setHost(config.getHost());
|
||||
factory.setPort(config.getPort());
|
||||
}
|
||||
if (config.getUsername() != null) {
|
||||
factory.setUsername(config.getUsername());
|
||||
}
|
||||
if (config.getPassword() != null) {
|
||||
factory.setPassword(config.getPassword());
|
||||
}
|
||||
if (config.getVirtualHost() != null) {
|
||||
factory.setVirtualHost(config.getVirtualHost());
|
||||
}
|
||||
return factory;
|
||||
}
|
||||
|
||||
// If no RabbitProperties bean is available, instantiate one, deferring to Spring Boot for populating it
|
||||
@Configuration
|
||||
@ConditionalOnMissingBean(RabbitProperties.class)
|
||||
@EnableConfigurationProperties(RabbitProperties.class)
|
||||
public static class RabbitPropertiesLoader {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RabbitConnectionFactoryBean rabbitFactory() {
|
||||
RabbitConnectionFactoryBean rabbitConnectionFactoryBean = new RabbitConnectionFactoryBean();
|
||||
rabbitConnectionFactoryBean.setUseSSL(this.useSSL);
|
||||
rabbitConnectionFactoryBean.setSslPropertiesLocation(this.sslPropertiesLocation);
|
||||
return rabbitConnectionFactoryBean;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.rabbit;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.amqp.AmqpException;
|
||||
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.connection.Connection;
|
||||
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.connection.ConnectionListener;
|
||||
import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean;
|
||||
import org.springframework.amqp.rabbit.connection.RoutingConnectionFactory;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.springframework.xd.dirt.integration.bus.RabbitManagementUtils;
|
||||
|
||||
|
||||
/**
|
||||
* A {@link RoutingConnectionFactory} that determines the node on which a queue is located and
|
||||
* returns a factory that connects directly to that node.
|
||||
* The RabbitMQ management plugin is called over REST to determine the node and the corresponding
|
||||
* address for that node is injected into the connection factory.
|
||||
* A single instance of each connection factory is retained in a cache.
|
||||
* If the location cannot be determined, the default connection factory is returned. This connection
|
||||
* factory is typically configured to connect to all the servers in a fail-over mode.
|
||||
* <p>{@link #getTargetConnectionFactory(Object)} is invoked by the
|
||||
* {@code SimpleMessageListenerContainer}, when establishing a connection, with the lookup key having
|
||||
* the format {@code '[queueName]'}.
|
||||
* <p>All {@link ConnectionFactory} methods delegate to the default
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 1.2
|
||||
*/
|
||||
public class LocalizedQueueConnectionFactory implements ConnectionFactory, RoutingConnectionFactory {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final Map<String, ConnectionFactory> nodeFactories = new HashMap<>();
|
||||
|
||||
private final ConnectionFactory defaultConnectionFactory;
|
||||
|
||||
private final String[] addresses;
|
||||
|
||||
private final String[] adminAdresses;
|
||||
|
||||
private final String[] nodes;
|
||||
|
||||
private final String vhost;
|
||||
|
||||
private final String username;
|
||||
|
||||
private final String password;
|
||||
|
||||
private final boolean useSSL;
|
||||
|
||||
private final Resource sslPropertiesLocation;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param defaultConnectionFactory the fallback connection factory to use if the queue can't be located.
|
||||
* @param addresses the rabbitmq server addresses (host:port, ...).
|
||||
* @param adminAddresses the rabbitmq admin addresses (http://host:port, ...) must be the same length
|
||||
* as addresses.
|
||||
* @param nodes the rabbitmq nodes corresponding to addresses (rabbit@server1, ...).
|
||||
* @param vhost the virtual host.
|
||||
* @param username the user name.
|
||||
* @param password the password.
|
||||
*/
|
||||
public LocalizedQueueConnectionFactory(ConnectionFactory defaultConnectionFactory,
|
||||
String[] addresses, String[] adminAddresses, String[] nodes, String vhost,
|
||||
String username, String password, boolean useSSL, Resource sslPropertiesLocation) {
|
||||
Assert.isTrue(addresses.length == adminAddresses.length
|
||||
&& addresses.length == nodes.length,
|
||||
"'addresses', 'adminAddresses', and 'nodes' properties must have equal length");
|
||||
this.defaultConnectionFactory = defaultConnectionFactory;
|
||||
this.addresses = Arrays.copyOf(addresses, addresses.length);
|
||||
this.adminAdresses = Arrays.copyOf(adminAddresses, adminAddresses.length);
|
||||
this.nodes = Arrays.copyOf(nodes, nodes.length);
|
||||
this.vhost = vhost;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.useSSL = useSSL;
|
||||
this.sslPropertiesLocation = sslPropertiesLocation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection createConnection() throws AmqpException {
|
||||
return this.defaultConnectionFactory.createConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
return this.defaultConnectionFactory.getHost();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPort() {
|
||||
return this.defaultConnectionFactory.getPort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVirtualHost() {
|
||||
return this.vhost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addConnectionListener(ConnectionListener listener) {
|
||||
this.defaultConnectionFactory.addConnectionListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeConnectionListener(ConnectionListener listener) {
|
||||
return this.defaultConnectionFactory.removeConnectionListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearConnectionListeners() {
|
||||
this.defaultConnectionFactory.clearConnectionListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConnectionFactory getTargetConnectionFactory(Object key) {
|
||||
String queue = ((String) key);
|
||||
queue = queue.substring(1, queue.length() - 1);
|
||||
ConnectionFactory connectionFactory = determineConnectionFactory(queue);
|
||||
if (connectionFactory == null) {
|
||||
return this.defaultConnectionFactory;
|
||||
}
|
||||
else {
|
||||
return connectionFactory;
|
||||
}
|
||||
}
|
||||
|
||||
private ConnectionFactory determineConnectionFactory(String queue) {
|
||||
for (int i = 0; i < this.adminAdresses.length; i++) {
|
||||
String adminUri = this.adminAdresses[i];
|
||||
RestTemplate template = createRestTemplate(adminUri);
|
||||
URI uri = UriComponentsBuilder.fromUriString(adminUri + "/api")
|
||||
.pathSegment("queues", "{vhost}", "{queue}")
|
||||
.buildAndExpand(this.vhost, queue).encode().toUri();
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> queueInfo = template.getForObject(uri, Map.class);
|
||||
if (queueInfo != null) {
|
||||
String node = (String) queueInfo.get("node");
|
||||
if (node != null) {
|
||||
for (int j = 0; j < this.nodes.length; j++) {
|
||||
if (this.nodes[j].equals(node)) {
|
||||
return nodeConnectionFactory(queue, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
logger.error("Failed to determine queue location for: " + queue + " at: " +
|
||||
uri.toString(), e);
|
||||
}
|
||||
}
|
||||
logger.warn("Failed to determine queue location for: " + queue);
|
||||
return null;
|
||||
}
|
||||
|
||||
private synchronized ConnectionFactory nodeConnectionFactory(String queue, int index) throws Exception {
|
||||
String address = this.addresses[index];
|
||||
String node = this.nodes[index];
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Queue: " + queue + " is on node: " + node + " at: " + address);
|
||||
}
|
||||
ConnectionFactory cf = this.nodeFactories.get(node);
|
||||
if (cf == null) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Creating new connection factory for: " + address);
|
||||
}
|
||||
cf = createConnectionFactory(address);
|
||||
this.nodeFactories.put(node, cf);
|
||||
}
|
||||
return cf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a RestTemplate for the supplied URI.
|
||||
* @param adminUri the URI.
|
||||
* @return the template.
|
||||
*/
|
||||
protected RestTemplate createRestTemplate(String adminUri) {
|
||||
return RabbitManagementUtils.buildRestTemplate(adminUri, this.username, this.password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dedicated connection factory for the address.
|
||||
* @param address the address to which the factory should connect.
|
||||
* @return the connection factory.
|
||||
* @throws Exception if errors occur during creation.
|
||||
*/
|
||||
protected ConnectionFactory createConnectionFactory(String address) throws Exception {
|
||||
RabbitConnectionFactoryBean rcfb = new RabbitConnectionFactoryBean();
|
||||
rcfb.setUseSSL(this.useSSL);
|
||||
rcfb.setSslPropertiesLocation(this.sslPropertiesLocation);
|
||||
rcfb.afterPropertiesSet();
|
||||
CachingConnectionFactory ccf = new CachingConnectionFactory(rcfb.getObject());
|
||||
ccf.setAddresses(address);
|
||||
ccf.setUsername(this.username);
|
||||
ccf.setPassword(this.password);
|
||||
ccf.setVirtualHost(this.vhost);
|
||||
return ccf;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.rabbit;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.springframework.xd.dirt.integration.bus.BusCleaner;
|
||||
import org.springframework.xd.dirt.integration.bus.BusUtils;
|
||||
import org.springframework.xd.dirt.integration.bus.MessageBusSupport;
|
||||
import org.springframework.xd.dirt.integration.bus.RabbitAdminException;
|
||||
import org.springframework.xd.dirt.integration.bus.RabbitManagementUtils;
|
||||
|
||||
|
||||
/**
|
||||
* Implementation of {@link org.springframework.xd.dirt.integration.bus.BusCleaner} for the {@code RabbitMessageBus}.
|
||||
* @author Gary Russell
|
||||
* @author David Turanski
|
||||
* @since 1.2
|
||||
*/
|
||||
public class RabbitBusCleaner implements BusCleaner {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(RabbitBusCleaner.class);
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> clean(String entity, boolean isJob) {
|
||||
return clean("http://localhost:15672", "guest", "guest", "/", "xdbus.", entity, isJob);
|
||||
}
|
||||
|
||||
public Map<String, List<String>> clean(String adminUri, String user, String pw, String vhost,
|
||||
String busPrefix, String entity, boolean isJob) {
|
||||
return doClean(
|
||||
adminUri == null ? "http://localhost:15672" : adminUri,
|
||||
user == null ? "guest" : user,
|
||||
pw == null ? "guest" : pw,
|
||||
vhost == null ? "/" : vhost,
|
||||
busPrefix == null ? "xdbus." : busPrefix,
|
||||
entity, isJob);
|
||||
}
|
||||
|
||||
private Map<String, List<String>> doClean(String adminUri, String user, String pw, String vhost,
|
||||
String busPrefix, String entity, boolean isJob) {
|
||||
RestTemplate restTemplate = RabbitManagementUtils.buildRestTemplate(adminUri, user, pw);
|
||||
List<String> removedQueues = isJob
|
||||
? null//findJobQueues(adminUri, vhost, busPrefix, entity, restTemplate)
|
||||
: findStreamQueues(adminUri, vhost, busPrefix, entity, restTemplate);
|
||||
ExchangeCandidateCallback callback = null;
|
||||
if (isJob) {
|
||||
// String pattern;
|
||||
// if (entity.endsWith("*")) {
|
||||
// pattern = entity.substring(0, entity.length() - 1) + "[^.]*";
|
||||
// }
|
||||
// else {
|
||||
// pattern = entity;
|
||||
// }
|
||||
// Collection<String> exchangeNames = JobEventsListenerPlugin.getEventListenerChannels(pattern).values();
|
||||
// final Set<Pattern> jobExchanges = new HashSet<>();
|
||||
// for (String exchange : exchangeNames) {
|
||||
// jobExchanges.add(Pattern.compile(MessageBusSupport.applyPrefix(busPrefix,
|
||||
// MessageBusSupport.applyPubSub(exchange))));
|
||||
// }
|
||||
// jobExchanges.add(Pattern.compile(MessageBusSupport.applyPrefix(busPrefix, MessageBusSupport.applyPubSub(
|
||||
// JobEventsListenerPlugin.getEventListenerChannelName(pattern)))));
|
||||
// callback = new ExchangeCandidateCallback() {
|
||||
//
|
||||
// @Override
|
||||
// public boolean isCandidate(String exchangeName) {
|
||||
// for (Pattern pattern : jobExchanges) {
|
||||
// Matcher matcher = pattern.matcher(exchangeName);
|
||||
// if (matcher.matches()) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// };
|
||||
}
|
||||
else {
|
||||
final String tapPrefix = adjustPrefix(MessageBusSupport.applyPrefix(busPrefix,
|
||||
MessageBusSupport.applyPubSub(BusUtils.constructTapPrefix(entity))));
|
||||
callback = new ExchangeCandidateCallback() {
|
||||
|
||||
@Override
|
||||
public boolean isCandidate(String exchangeName) {
|
||||
return exchangeName.startsWith(tapPrefix);
|
||||
}
|
||||
};
|
||||
}
|
||||
List<String> removedExchanges = findExchanges(adminUri, vhost, busPrefix, entity, restTemplate, callback);
|
||||
// Delete the queues in reverse order to enable re-running after a partial success.
|
||||
// The queue search above starts with 0 and terminates on a not found.
|
||||
for (int i = removedQueues.size() - 1; i >= 0; i--) {
|
||||
String queueName = removedQueues.get(i);
|
||||
URI uri = UriComponentsBuilder.fromUriString(adminUri + "/api")
|
||||
.pathSegment("queues", "{vhost}", "{stream}")
|
||||
.buildAndExpand(vhost, queueName).encode().toUri();
|
||||
restTemplate.delete(uri);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("deleted queue: " + queueName);
|
||||
}
|
||||
}
|
||||
Map<String, List<String>> results = new HashMap<>();
|
||||
if (removedQueues.size() > 0) {
|
||||
results.put("queues", removedQueues);
|
||||
}
|
||||
// Fanout exchanges for taps
|
||||
for (String exchange : removedExchanges) {
|
||||
URI uri = UriComponentsBuilder.fromUriString(adminUri + "/api")
|
||||
.pathSegment("exchanges", "{vhost}", "{name}")
|
||||
.buildAndExpand(vhost, exchange).encode().toUri();
|
||||
restTemplate.delete(uri);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("deleted exchange: " + exchange);
|
||||
}
|
||||
}
|
||||
if (removedExchanges.size() > 0) {
|
||||
results.put("exchanges", removedExchanges);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private List<String> findStreamQueues(String adminUri, String vhost, String busPrefix, String stream,
|
||||
RestTemplate restTemplate) {
|
||||
String queueNamePrefix = adjustPrefix(MessageBusSupport.applyPrefix(busPrefix, stream));
|
||||
List<Map<String, Object>> queues = listAllQueues(adminUri, vhost, restTemplate);
|
||||
List<String> removedQueues = new ArrayList<>();
|
||||
for (Map<String, Object> queue : queues) {
|
||||
String queueName = (String) queue.get("name");
|
||||
if (queueName.startsWith(queueNamePrefix)) {
|
||||
checkNoConsumers(queueName, queue);
|
||||
removedQueues.add(queueName);
|
||||
}
|
||||
}
|
||||
return removedQueues;
|
||||
}
|
||||
|
||||
// private List<String> findJobQueues(String adminUri, String vhost, String busPrefix, String job,
|
||||
// RestTemplate restTemplate) {
|
||||
// List<String> removedQueues = new ArrayList<>();
|
||||
// String jobQueueName = MessageBusSupport.applyPrefix(busPrefix,
|
||||
// AbstractJobPlugin.getJobChannelName(job));
|
||||
// String jobRequestsQueuePrefix = adjustPrefix(MessageBusSupport.applyPrefix(busPrefix,
|
||||
// AbstractJobPlugin.getJobChannelName(job)));
|
||||
// List<Map<String, Object>> queues = listAllQueues(adminUri, vhost, restTemplate);
|
||||
// for (Map<String, Object> queue : queues) {
|
||||
// String queueName = (String) queue.get("name");
|
||||
// if (job.endsWith("*")) {
|
||||
// if (queueName.startsWith(jobQueueName.substring(0, jobQueueName.length() - 1))) {
|
||||
// checkNoConsumers(queueName, queue);
|
||||
// removedQueues.add(queueName);
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// if (queueName.equals(jobQueueName)) {
|
||||
// checkNoConsumers(queueName, queue);
|
||||
// removedQueues.add(queueName);
|
||||
// }
|
||||
// else if (queueName.startsWith(jobRequestsQueuePrefix)
|
||||
// && queueName.endsWith(MessageBusSupport.applyRequests(""))) {
|
||||
// checkNoConsumers(queueName, queue);
|
||||
// removedQueues.add(queueName);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return removedQueues;
|
||||
// }
|
||||
|
||||
private List<Map<String, Object>> listAllQueues(String adminUri, String vhost, RestTemplate restTemplate) {
|
||||
URI uri = UriComponentsBuilder.fromUriString(adminUri + "/api")
|
||||
.pathSegment("queues", "{vhost}")
|
||||
.buildAndExpand(vhost).encode().toUri();
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> queues = restTemplate.getForObject(uri, List.class);
|
||||
return queues;
|
||||
}
|
||||
|
||||
private String adjustPrefix(String prefix) {
|
||||
if (prefix.endsWith("*")) {
|
||||
return prefix.substring(0, prefix.length() - 1);
|
||||
}
|
||||
else {
|
||||
return prefix + BusUtils.GROUP_INDEX_DELIMITER;
|
||||
}
|
||||
}
|
||||
|
||||
private void checkNoConsumers(String queueName, Map<String, Object> queue) {
|
||||
if (!queue.get("consumers").equals(Integer.valueOf(0))) {
|
||||
throw new RabbitAdminException("Queue " + queueName + " is in use");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<String> findExchanges(String adminUri, String vhost, String busPrefix, String entity,
|
||||
RestTemplate restTemplate, ExchangeCandidateCallback callback) {
|
||||
List<String> removedExchanges = new ArrayList<>();
|
||||
URI uri = UriComponentsBuilder.fromUriString(adminUri + "/api")
|
||||
.pathSegment("exchanges", "{vhost}")
|
||||
.buildAndExpand(vhost).encode().toUri();
|
||||
List<Map<String, Object>> exchanges = restTemplate.getForObject(uri, List.class);
|
||||
for (Map<String, Object> exchange : exchanges) {
|
||||
String exchangeName = (String) exchange.get("name");
|
||||
if (callback.isCandidate(exchangeName)) {
|
||||
uri = UriComponentsBuilder.fromUriString(adminUri + "/api")
|
||||
.pathSegment("exchanges", "{vhost}", "{name}", "bindings", "source")
|
||||
.buildAndExpand(vhost, exchangeName).encode().toUri();
|
||||
List<Map<String, Object>> bindings = restTemplate.getForObject(uri, List.class);
|
||||
if (bindings.size() == 0) {
|
||||
uri = UriComponentsBuilder.fromUriString(adminUri + "/api")
|
||||
.pathSegment("exchanges", "{vhost}", "{name}", "bindings", "destination")
|
||||
.buildAndExpand(vhost, exchangeName).encode().toUri();
|
||||
bindings = restTemplate.getForObject(uri, List.class);
|
||||
if (bindings.size() == 0) {
|
||||
removedExchanges.add((String) exchange.get("name"));
|
||||
}
|
||||
else {
|
||||
throw new RabbitAdminException("Cannot delete exchange " + exchangeName
|
||||
+ "; it is a destination: " + bindings);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new RabbitAdminException("Cannot delete exchange " + exchangeName + "; it has bindings: "
|
||||
+ bindings);
|
||||
}
|
||||
}
|
||||
}
|
||||
return removedExchanges;
|
||||
}
|
||||
|
||||
private interface ExchangeCandidateCallback {
|
||||
|
||||
boolean isCandidate(String exchangeName);
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* This package contains an implementation of the {@link org.springframework.xd.dirt.integration.bus.MessageBus} for RabbitMQ.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.rabbit;
|
||||
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:context="http://www.springframework.org/schema/context"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
|
||||
|
||||
<bean class="org.springframework.xd.dirt.integration.rabbit.ConnectionFactorySettings" />
|
||||
|
||||
<bean id="messageBus" class="org.springframework.xd.dirt.integration.rabbit.RabbitMessageBus">
|
||||
<constructor-arg ref="rabbitConnectionFactory" />
|
||||
<constructor-arg ref="codec"/>
|
||||
<property name="defaultAcknowledgeMode" value="#{T(org.springframework.amqp.core.AcknowledgeMode).${xd.messagebus.rabbit.default.ackMode}}" />
|
||||
<property name="defaultBackOffInitialInterval" value="${xd.messagebus.rabbit.default.backOffInitialInterval}" />
|
||||
<property name="defaultBackOffMaxInterval" value="${xd.messagebus.rabbit.default.backOffMaxInterval}" />
|
||||
<property name="defaultBackOffMultiplier" value="${xd.messagebus.rabbit.default.backOffMultiplier}" />
|
||||
<property name="defaultChannelTransacted" value="${xd.messagebus.rabbit.default.transacted}" />
|
||||
<property name="defaultConcurrency" value="${xd.messagebus.rabbit.default.concurrency}" />
|
||||
<property name="defaultDefaultDeliveryMode" value="#{T(org.springframework.amqp.core.MessageDeliveryMode).${xd.messagebus.rabbit.default.deliveryMode}}" />
|
||||
<property name="defaultDefaultRequeueRejected" value="${xd.messagebus.rabbit.default.requeue}" />
|
||||
<property name="defaultMaxAttempts" value="${xd.messagebus.rabbit.default.maxAttempts}" />
|
||||
<property name="defaultMaxConcurrency" value="${xd.messagebus.rabbit.default.maxConcurrency}" />
|
||||
<property name="defaultPrefetchCount" value="${xd.messagebus.rabbit.default.prefetch}" />
|
||||
<property name="defaultPrefix" value="${xd.messagebus.rabbit.default.prefix}" />
|
||||
<property name="defaultReplyHeaderPatterns" value="${xd.messagebus.rabbit.default.replyHeaderPatterns}" />
|
||||
<property name="defaultRequestHeaderPatterns" value="${xd.messagebus.rabbit.default.requestHeaderPatterns}" />
|
||||
<property name="defaultTxSize" value="${xd.messagebus.rabbit.default.txSize}" />
|
||||
<property name="defaultAutoBindDLQ" value="${xd.messagebus.rabbit.default.autoBindDLQ}" />
|
||||
<property name="defaultRepublishToDLQ" value="${xd.messagebus.rabbit.default.republishToDLQ}" />
|
||||
<property name="defaultBatchingEnabled" value="${xd.messagebus.rabbit.default.batchingEnabled}" />
|
||||
<property name="defaultBatchSize" value="${xd.messagebus.rabbit.default.batchSize}" />
|
||||
<property name="defaultBatchBufferLimit" value="${xd.messagebus.rabbit.default.batchBufferLimit}" />
|
||||
<property name="defaultBatchTimeout" value="${xd.messagebus.rabbit.default.batchTimeout}" />
|
||||
<property name="defaultCompress" value="${xd.messagebus.rabbit.default.compress}" />
|
||||
<property name="compressingPostProcessor">
|
||||
<bean class="org.springframework.amqp.support.postprocessor.GZipPostProcessor">
|
||||
<property name="level" value="${xd.messagebus.rabbit.compressionLevel:#{T(java.util.zip.Deflater).BEST_SPEED}}" />
|
||||
</bean>
|
||||
</property>
|
||||
<property name="decompressingPostProcessor">
|
||||
<bean class="org.springframework.amqp.support.postprocessor.DelegatingDecompressingPostProcessor">
|
||||
<!-- set a map of decompressors here if using other than the default -->
|
||||
</bean>
|
||||
</property>
|
||||
<property name="defaultDurableSubscription" value="${xd.messagebus.rabbit.default.durableSubscription}" />
|
||||
<property name="addresses" value="${spring.rabbitmq.addresses:}" />
|
||||
<property name="adminAddresses" value="${spring.rabbitmq.adminAddresses:}" />
|
||||
<property name="nodes" value="${spring.rabbitmq.nodes:}" />
|
||||
<property name="username" value="${spring.rabbitmq.username:}" />
|
||||
<property name="password" value="${spring.rabbitmq.password:}" />
|
||||
<property name="vhost" value="${spring.rabbitmq.virtual_host:}" />
|
||||
<property name="useSSL" value="${spring.rabbitmq.useSSL:false}" />
|
||||
<property name="sslPropertiesLocation" value="${spring.rabbitmq.sslProperties:}" />
|
||||
</bean>
|
||||
|
||||
</beans>
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.rabbit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.amqp.core.Queue;
|
||||
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.core.RabbitAdmin;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class LocalizedQueueConnectionFactoryIntegrationTests {
|
||||
|
||||
@ClassRule
|
||||
public static RabbitTestSupport rabbitAvailableRule = new RabbitTestSupport(true);
|
||||
|
||||
private LocalizedQueueConnectionFactory lqcf;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
ConnectionFactory defaultConnectionFactory = new CachingConnectionFactory("localhost");
|
||||
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" };
|
||||
String vhost = "/";
|
||||
String username = "guest";
|
||||
String password = "guest";
|
||||
this.lqcf = new LocalizedQueueConnectionFactory(defaultConnectionFactory, addresses,
|
||||
adminAddresses, nodes, vhost, username, password, false, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnect() {
|
||||
RabbitAdmin admin = new RabbitAdmin(this.lqcf);
|
||||
Queue queue = new Queue(UUID.randomUUID().toString(), false, false, true);
|
||||
admin.declareQueue(queue);
|
||||
ConnectionFactory targetConnectionFactory = this.lqcf.getTargetConnectionFactory("[" + queue.getName() + "]");
|
||||
RabbitTemplate template = new RabbitTemplate(targetConnectionFactory);
|
||||
template.convertAndSend("", queue.getName(), "foo");
|
||||
assertEquals("foo", template.receiveAndConvert(queue.getName()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.rabbit;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyBoolean;
|
||||
import static org.mockito.Matchers.anyMap;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Mockito.atLeast;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Matchers;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import org.springframework.amqp.rabbit.connection.Connection;
|
||||
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
|
||||
import org.springframework.amqp.utils.test.TestUtils;
|
||||
import org.springframework.beans.DirectFieldAccessor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import com.rabbitmq.client.Channel;
|
||||
import com.rabbitmq.client.Consumer;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class LocalizedQueueConnectionFactoryTests {
|
||||
|
||||
private final Map<String, ConnectionFactory> cfs = new HashMap<>();
|
||||
|
||||
private final Map<String, Connection> connections = new HashMap<>();
|
||||
|
||||
private final Map<String, Channel> channels = new HashMap<>();
|
||||
|
||||
private final Map<String, Consumer> consumers = new HashMap<>();
|
||||
|
||||
private final Map<String, String> consumerTags = new HashMap<>();
|
||||
|
||||
private final CountDownLatch latch = new CountDownLatch(2);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testFailOver() throws Exception {
|
||||
ConnectionFactory defaultConnectionFactory = mockCF("localhost:1234");
|
||||
String rabbit1 = "localhost:1235";
|
||||
String rabbit2 = "localhost:1236";
|
||||
String[] addresses = new String[] { rabbit1, rabbit2 };
|
||||
String[] adminAddresses = new String[] { "http://localhost:11235", "http://localhost:11236" };
|
||||
String[] nodes = new String[] { "rabbit@foo", "rabbit@bar" };
|
||||
String vhost = "/";
|
||||
String username = "guest";
|
||||
String password = "guest";
|
||||
final AtomicBoolean firstServer = new AtomicBoolean(true);
|
||||
LocalizedQueueConnectionFactory lqcf = new LocalizedQueueConnectionFactory(defaultConnectionFactory, addresses,
|
||||
adminAddresses, nodes, vhost, username, password, false, null) {
|
||||
|
||||
private final String[] nodes = new String[] { "rabbit@foo", "rabbit@bar" };
|
||||
|
||||
@Override
|
||||
protected RestTemplate createRestTemplate(String adminUri) {
|
||||
return doCreateRestTemplate(adminUri, firstServer.get() ? nodes[0] : nodes[1]);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConnectionFactory createConnectionFactory(String address) throws Exception {
|
||||
return mockCF(address);
|
||||
}
|
||||
|
||||
};
|
||||
Log logger = spy(TestUtils.getPropertyValue(lqcf, "logger", Log.class));
|
||||
new DirectFieldAccessor(lqcf).setPropertyValue("logger", logger);
|
||||
when(logger.isDebugEnabled()).thenReturn(true);
|
||||
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
|
||||
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(lqcf);
|
||||
container.setQueueNames("q");
|
||||
container.afterPropertiesSet();
|
||||
container.start();
|
||||
Channel channel = this.channels.get(rabbit1);
|
||||
verify(channel).basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(),
|
||||
anyBoolean(), anyMap(),
|
||||
Matchers.any(Consumer.class));
|
||||
verify(logger, atLeast(1)).debug(captor.capture());
|
||||
assertTrue(assertLog(captor.getAllValues(), "Queue: q is on node: rabbit@foo at: localhost:1235"));
|
||||
|
||||
// Fail rabbit1 and verify the container switches to rabbit2
|
||||
|
||||
firstServer.set(false);
|
||||
when(channel.isOpen()).thenReturn(false);
|
||||
when(this.connections.get(rabbit1).isOpen()).thenReturn(false);
|
||||
this.consumers.get(rabbit1).handleCancel(consumerTags.get(rabbit1));
|
||||
assertTrue(latch.await(10, TimeUnit.SECONDS));
|
||||
channel = this.channels.get(rabbit2);
|
||||
verify(channel).basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(),
|
||||
anyBoolean(), anyMap(),
|
||||
Matchers.any(Consumer.class));
|
||||
container.stop();
|
||||
verify(logger, atLeast(1)).debug(captor.capture());
|
||||
assertTrue(assertLog(captor.getAllValues(), "Queue: q is on node: rabbit@bar at: localhost:1236"));
|
||||
}
|
||||
|
||||
private boolean assertLog(List<String> logRows, String expected) {
|
||||
for (String log : logRows) {
|
||||
if (log.contains(expected)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private RestTemplate doCreateRestTemplate(String uri, String node) {
|
||||
RestTemplate template = new RestTemplate();
|
||||
MockRestServiceServer server = MockRestServiceServer.createServer(template);
|
||||
server.expect(requestTo(uri + "/api/queues/%2F/q"))
|
||||
.andRespond(withSuccess("{ \"node\" : \""
|
||||
+ node
|
||||
+ "\" }", MediaType.APPLICATION_JSON));
|
||||
return template;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ConnectionFactory mockCF(final String address) throws Exception {
|
||||
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
|
||||
Connection connection = mock(Connection.class);
|
||||
Channel channel = mock(Channel.class);
|
||||
when(connectionFactory.createConnection()).thenReturn(connection);
|
||||
when(connection.createChannel(false)).thenReturn(channel);
|
||||
when(connection.isOpen()).thenReturn(true);
|
||||
when(channel.isOpen()).thenReturn(true);
|
||||
doAnswer(new Answer<String>() {
|
||||
|
||||
@Override
|
||||
public String answer(InvocationOnMock invocation) throws Throwable {
|
||||
String tag = UUID.randomUUID().toString();
|
||||
consumers.put(address, (Consumer) invocation.getArguments()[6]);
|
||||
consumerTags.put(address, tag);
|
||||
latch.countDown();
|
||||
return tag;
|
||||
}
|
||||
}).when(channel).basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), anyMap(),
|
||||
any(Consumer.class));
|
||||
when(connectionFactory.getHost()).thenReturn(address);
|
||||
this.cfs.put(address, connectionFactory);
|
||||
this.connections.put(address, connection);
|
||||
this.channels.put(address, channel);
|
||||
return connectionFactory;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.rabbit;
|
||||
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.xd.test.AbstractExternalResourceTestSupport;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* JUnit {@link org.junit.Rule} that detects the fact that RabbitMQ is available on localhost with
|
||||
* the management plugin enabled.
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 1.2
|
||||
*/
|
||||
public class RabbitAdminTestSupport extends AbstractExternalResourceTestSupport<RestTemplate> {
|
||||
|
||||
public RabbitAdminTestSupport() {
|
||||
super("RABBITADMIN");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void obtainResource() throws Exception {
|
||||
resource = new RestTemplate();
|
||||
try {
|
||||
resource.getForObject("http://localhost:15672/api/overview", Map.class);
|
||||
}
|
||||
catch (HttpClientErrorException e) {
|
||||
if (e.getStatusCode() != HttpStatus.UNAUTHORIZED) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanupResource() throws Exception {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.rabbit;
|
||||
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.rabbitmq.client.Channel;
|
||||
import com.rabbitmq.client.DefaultConsumer;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.amqp.core.BindingBuilder;
|
||||
import org.springframework.amqp.core.FanoutExchange;
|
||||
import org.springframework.amqp.core.Queue;
|
||||
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.core.ChannelCallback;
|
||||
import org.springframework.amqp.rabbit.core.RabbitAdmin;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.springframework.xd.dirt.integration.bus.BusUtils;
|
||||
import org.springframework.xd.dirt.integration.bus.MessageBusSupport;
|
||||
import org.springframework.xd.dirt.integration.bus.RabbitAdminException;
|
||||
import org.springframework.xd.dirt.integration.bus.RabbitManagementUtils;
|
||||
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @since 1.2
|
||||
*/
|
||||
public class RabbitBusCleanerTests {
|
||||
|
||||
private static final String XDBUS_PREFIX = "xdbus.";
|
||||
|
||||
@Rule
|
||||
public RabbitAdminTestSupport adminTest = new RabbitAdminTestSupport();
|
||||
|
||||
@Rule
|
||||
public RabbitTestSupport test = new RabbitTestSupport();
|
||||
|
||||
@Test
|
||||
public void testCleanStream() {
|
||||
final RabbitBusCleaner cleaner = new RabbitBusCleaner();
|
||||
final RestTemplate template = RabbitManagementUtils.buildRestTemplate("http://localhost:15672", "guest",
|
||||
"guest");
|
||||
final String stream1 = UUID.randomUUID().toString();
|
||||
String stream2 = stream1 + "-1";
|
||||
String firstQueue = null;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
String queue1Name = MessageBusSupport.applyPrefix(XDBUS_PREFIX,
|
||||
BusUtils.constructPipeName(stream1, i));
|
||||
String queue2Name = MessageBusSupport.applyPrefix(XDBUS_PREFIX,
|
||||
BusUtils.constructPipeName(stream2, i));
|
||||
if (firstQueue == null) {
|
||||
firstQueue = queue1Name;
|
||||
}
|
||||
URI uri = UriComponentsBuilder.fromUriString("http://localhost:15672/api/queues")
|
||||
.pathSegment("{vhost}", "{queue}")
|
||||
.buildAndExpand("/", queue1Name)
|
||||
.encode().toUri();
|
||||
template.put(uri, new AmqpQueue(false, true));
|
||||
uri = UriComponentsBuilder.fromUriString("http://localhost:15672/api/queues")
|
||||
.pathSegment("{vhost}", "{queue}")
|
||||
.buildAndExpand("/", queue2Name)
|
||||
.encode().toUri();
|
||||
template.put(uri, new AmqpQueue(false, true));
|
||||
uri = UriComponentsBuilder.fromUriString("http://localhost:15672/api/queues")
|
||||
.pathSegment("{vhost}", "{queue}")
|
||||
.buildAndExpand("/", MessageBusSupport.constructDLQName(queue1Name)).encode().toUri();
|
||||
template.put(uri, new AmqpQueue(false, true));
|
||||
}
|
||||
CachingConnectionFactory connectionFactory = test.getResource();
|
||||
RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
|
||||
final FanoutExchange fanout1 = new FanoutExchange(
|
||||
MessageBusSupport.applyPrefix(XDBUS_PREFIX, MessageBusSupport.applyPubSub(
|
||||
BusUtils.constructTapPrefix(stream1) + ".foo.bar")));
|
||||
rabbitAdmin.declareExchange(fanout1);
|
||||
rabbitAdmin.declareBinding(BindingBuilder.bind(new Queue(firstQueue)).to(fanout1));
|
||||
final FanoutExchange fanout2 = new FanoutExchange(
|
||||
MessageBusSupport.applyPrefix(XDBUS_PREFIX, MessageBusSupport.applyPubSub(
|
||||
BusUtils.constructTapPrefix(stream2) + ".foo.bar")));
|
||||
rabbitAdmin.declareExchange(fanout2);
|
||||
rabbitAdmin.declareBinding(BindingBuilder.bind(new Queue(firstQueue)).to(fanout2));
|
||||
new RabbitTemplate(connectionFactory).execute(new ChannelCallback<Void>() {
|
||||
|
||||
@Override
|
||||
public Void doInRabbit(Channel channel) throws Exception {
|
||||
String queueName = MessageBusSupport.applyPrefix(XDBUS_PREFIX,
|
||||
BusUtils.constructPipeName(stream1, 4));
|
||||
String consumerTag = channel.basicConsume(queueName, new DefaultConsumer(channel));
|
||||
try {
|
||||
waitForConsumerStateNot(queueName, 0);
|
||||
cleaner.clean(stream1, false);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (RabbitAdminException e) {
|
||||
assertEquals("Queue " + queueName + " is in use", e.getMessage());
|
||||
}
|
||||
channel.basicCancel(consumerTag);
|
||||
waitForConsumerStateNot(queueName, 1);
|
||||
try {
|
||||
cleaner.clean(stream1, false);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (RabbitAdminException e) {
|
||||
assertThat(e.getMessage(), startsWith("Cannot delete exchange " +
|
||||
fanout1.getName() + "; it has bindings:"));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void waitForConsumerStateNot(String queueName, int state) throws InterruptedException {
|
||||
int n = 0;
|
||||
URI uri = UriComponentsBuilder.fromUriString("http://localhost:15672/api/queues").pathSegment(
|
||||
"{vhost}", "{queue}")
|
||||
.buildAndExpand("/", queueName).encode().toUri();
|
||||
while (n++ < 100) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> queueInfo = template.getForObject(uri, Map.class);
|
||||
if (!queueInfo.get("consumers").equals(Integer.valueOf(state))) {
|
||||
break;
|
||||
}
|
||||
Thread.sleep(100);
|
||||
}
|
||||
assertTrue("Consumer state remained at " + state + " after 10 seconds", n < 100);
|
||||
}
|
||||
|
||||
});
|
||||
rabbitAdmin.deleteExchange(fanout1.getName()); // easier than deleting the binding
|
||||
rabbitAdmin.declareExchange(fanout1);
|
||||
connectionFactory.destroy();
|
||||
Map<String, List<String>> cleanedMap = cleaner.clean(stream1, false);
|
||||
assertEquals(2, cleanedMap.size());
|
||||
List<String> cleanedQueues = cleanedMap.get("queues");
|
||||
// should *not* clean stream2
|
||||
assertEquals(10, cleanedQueues.size());
|
||||
for (int i = 0; i < 5; i++) {
|
||||
assertEquals(XDBUS_PREFIX + stream1 + "." + i, cleanedQueues.get(i * 2));
|
||||
assertEquals(XDBUS_PREFIX + stream1 + "." + i + ".dlq", cleanedQueues.get(i * 2 + 1));
|
||||
}
|
||||
List<String> cleanedExchanges = cleanedMap.get("exchanges");
|
||||
assertEquals(1, cleanedExchanges.size());
|
||||
assertEquals(fanout1.getName(), cleanedExchanges.get(0));
|
||||
|
||||
// wild card *should* clean stream2
|
||||
cleanedMap = cleaner.clean(stream1 + "*", false);
|
||||
assertEquals(2, cleanedMap.size());
|
||||
cleanedQueues = cleanedMap.get("queues");
|
||||
assertEquals(5, cleanedQueues.size());
|
||||
for (int i = 0; i < 5; i++) {
|
||||
assertEquals(XDBUS_PREFIX + stream2 + "." + i, cleanedQueues.get(i));
|
||||
}
|
||||
cleanedExchanges = cleanedMap.get("exchanges");
|
||||
assertEquals(1, cleanedExchanges.size());
|
||||
assertEquals(fanout2.getName(), cleanedExchanges.get(0));
|
||||
}
|
||||
|
||||
public static class AmqpQueue {
|
||||
|
||||
private boolean autoDelete;
|
||||
|
||||
private boolean durable;
|
||||
|
||||
public AmqpQueue(boolean autoDelete, boolean durable) {
|
||||
this.autoDelete = autoDelete;
|
||||
this.durable = durable;
|
||||
}
|
||||
|
||||
|
||||
@JsonProperty("auto_delete")
|
||||
protected boolean isAutoDelete() {
|
||||
return autoDelete;
|
||||
}
|
||||
|
||||
|
||||
protected void setAutoDelete(boolean autoDelete) {
|
||||
this.autoDelete = autoDelete;
|
||||
}
|
||||
|
||||
|
||||
protected boolean isDurable() {
|
||||
return durable;
|
||||
}
|
||||
|
||||
|
||||
protected void setDurable(boolean durable) {
|
||||
this.durable = durable;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,715 @@
|
||||
/*
|
||||
* Copyright 2013-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.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;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.zip.Deflater;
|
||||
|
||||
import org.aopalliance.aop.Advice;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.amqp.core.AcknowledgeMode;
|
||||
import org.springframework.amqp.core.MessageDeliveryMode;
|
||||
import org.springframework.amqp.core.Queue;
|
||||
import org.springframework.amqp.rabbit.core.RabbitAdmin;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
|
||||
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.expression.spel.standard.SpelExpression;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.channel.QueueChannel;
|
||||
import org.springframework.integration.endpoint.AbstractEndpoint;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.xd.dirt.integration.bus.Binding;
|
||||
import org.springframework.xd.dirt.integration.bus.BusProperties;
|
||||
import org.springframework.xd.dirt.integration.bus.MessageBus;
|
||||
import org.springframework.xd.dirt.integration.bus.PartitionCapableBusTests;
|
||||
import org.springframework.xd.dirt.integration.bus.Spy;
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class RabbitMessageBusTests extends PartitionCapableBusTests {
|
||||
|
||||
@Rule
|
||||
public RabbitTestSupport rabbitAvailableRule = new RabbitTestSupport();
|
||||
|
||||
@Override
|
||||
protected MessageBus getMessageBus() {
|
||||
if (testMessageBus == null) {
|
||||
testMessageBus = new RabbitTestMessageBus(rabbitAvailableRule.getResource(), getCodec());
|
||||
}
|
||||
return testMessageBus;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean usesExplicitRouting() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendAndReceiveBad() throws Exception {
|
||||
MessageBus messageBus = getMessageBus();
|
||||
DirectChannel moduleOutputChannel = new DirectChannel();
|
||||
DirectChannel moduleInputChannel = new DirectChannel();
|
||||
messageBus.bindProducer("bad.0", moduleOutputChannel, null);
|
||||
messageBus.bindConsumer("bad.0", moduleInputChannel, null);
|
||||
Message<?> message = MessageBuilder.withPayload("bad").setHeader(MessageHeaders.CONTENT_TYPE, "foo/bar").build();
|
||||
final CountDownLatch latch = new CountDownLatch(3);
|
||||
moduleInputChannel.subscribe(new MessageHandler() {
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message<?> message) throws MessagingException {
|
||||
latch.countDown();
|
||||
throw new RuntimeException("bad");
|
||||
}
|
||||
});
|
||||
moduleOutputChannel.send(message);
|
||||
assertTrue(latch.await(10, TimeUnit.SECONDS));
|
||||
messageBus.unbindConsumers("bad.0");
|
||||
messageBus.unbindProducers("bad.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConsumerProperties() throws Exception {
|
||||
MessageBus bus = getMessageBus();
|
||||
Properties properties = new Properties();
|
||||
properties.put("transacted", "true"); // test transacted with defaults; not allowed with ackmode NONE
|
||||
bus.bindConsumer("props.0", new DirectChannel(), properties);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.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("xdbus.props.0", container.getQueueNames()[0]);
|
||||
assertTrue(TestUtils.getPropertyValue(container, "transactional", Boolean.class));
|
||||
assertEquals(1, TestUtils.getPropertyValue(container, "concurrentConsumers"));
|
||||
assertNull(TestUtils.getPropertyValue(container, "maxConcurrentConsumers"));
|
||||
assertTrue(TestUtils.getPropertyValue(container, "defaultRequeueRejected", Boolean.class));
|
||||
assertEquals(1, TestUtils.getPropertyValue(container, "prefetchCount"));
|
||||
assertEquals(1, TestUtils.getPropertyValue(container, "txSize"));
|
||||
Advice retry = TestUtils.getPropertyValue(container, "adviceChain", Advice[].class)[0];
|
||||
assertEquals(3, TestUtils.getPropertyValue(retry, "retryOperations.retryPolicy.maxAttempts"));
|
||||
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"));
|
||||
bus.unbindConsumers("props.0");
|
||||
assertEquals(0, bindings.size());
|
||||
|
||||
properties = new Properties();
|
||||
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("requestHeaderPatterns", "foo");
|
||||
properties.put("requeue", "false");
|
||||
properties.put("txSize", "10");
|
||||
properties.put("partitionIndex", 0);
|
||||
bus.bindConsumer("props.0", new DirectChannel(), properties);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Binding> bindingsNow = TestUtils.getPropertyValue(bus, "messageBus.bindings", List.class);
|
||||
assertEquals(1, bindingsNow.size());
|
||||
endpoint = bindingsNow.get(0).getEndpoint();
|
||||
container = verifyContainer(endpoint);
|
||||
|
||||
assertEquals("foo.props.0", container.getQueueNames()[0]);
|
||||
|
||||
try {
|
||||
bus.bindPubSubConsumer("dummy", null, properties);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertThat(e.getMessage(), allOf(
|
||||
containsString("RabbitMessageBus does not support consumer properties: "),
|
||||
containsString("partitionIndex"),
|
||||
containsString("concurrency"),
|
||||
containsString(" for dummy.")));
|
||||
}
|
||||
try {
|
||||
bus.bindConsumer("queue:dummy", null, properties);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertEquals("RabbitMessageBus does not support consumer property: partitionIndex for queue:dummy.",
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
bus.unbindConsumers("props.0");
|
||||
assertEquals(0, bindingsNow.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProducerProperties() throws Exception {
|
||||
MessageBus bus = getMessageBus();
|
||||
bus.bindProducer("props.0", new DirectChannel(), null);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.bindings", List.class);
|
||||
assertEquals(1, bindings.size());
|
||||
AbstractEndpoint endpoint = bindings.get(0).getEndpoint();
|
||||
assertEquals("xdbus.props.0", TestUtils.getPropertyValue(endpoint, "handler.delegate.routingKey"));
|
||||
MessageDeliveryMode mode = TestUtils.getPropertyValue(endpoint, "handler.delegate.defaultDeliveryMode",
|
||||
MessageDeliveryMode.class);
|
||||
assertEquals(MessageDeliveryMode.PERSISTENT, mode);
|
||||
List<?> requestHeaders = TestUtils.getPropertyValue(endpoint,
|
||||
"handler.delegate.headerMapper.requestHeaderMatcher.strategies", List.class);
|
||||
assertEquals(2, requestHeaders.size());
|
||||
bus.unbindProducers("props.0");
|
||||
assertEquals(0, bindings.size());
|
||||
|
||||
Properties properties = new Properties();
|
||||
properties.put("prefix", "foo.");
|
||||
properties.put("deliveryMode", "NON_PERSISTENT");
|
||||
properties.put("requestHeaderPatterns", "foo");
|
||||
properties.put("partitionKeyExpression", "'foo'");
|
||||
properties.put("partitionKeyExtractorClass", "foo");
|
||||
properties.put("partitionSelectorExpression", "0");
|
||||
properties.put("partitionSelectorClass", "foo");
|
||||
properties.put(BusProperties.NEXT_MODULE_COUNT, "1");
|
||||
|
||||
bus.bindProducer("props.0", new DirectChannel(), properties);
|
||||
assertEquals(1, bindings.size());
|
||||
endpoint = bindings.get(0).getEndpoint();
|
||||
assertEquals(
|
||||
"'foo.props.0-' + headers['partition']",
|
||||
TestUtils.getPropertyValue(endpoint, "handler.delegate.routingKeyExpression", SpelExpression.class).getExpressionString());
|
||||
mode = TestUtils.getPropertyValue(endpoint, "handler.delegate.defaultDeliveryMode",
|
||||
MessageDeliveryMode.class);
|
||||
assertEquals(MessageDeliveryMode.NON_PERSISTENT, mode);
|
||||
verifyFooRequestProducer(endpoint);
|
||||
|
||||
try {
|
||||
bus.bindPubSubProducer("dummy", new DirectChannel(), properties);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertThat(e.getMessage(), allOf(
|
||||
containsString("RabbitMessageBus does not support producer properties: "),
|
||||
containsString("partitionSelectorExpression"),
|
||||
containsString("partitionKeyExtractorClass"),
|
||||
containsString("partitionKeyExpression"),
|
||||
containsString("partitionSelectorClass")));
|
||||
assertThat(e.getMessage(), containsString("for dummy."));
|
||||
}
|
||||
try {
|
||||
bus.bindProducer("queue:dummy", new DirectChannel(), properties);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertThat(e.getMessage(), allOf(
|
||||
containsString("RabbitMessageBus does not support producer properties: "),
|
||||
containsString("partitionSelectorExpression"),
|
||||
containsString("partitionKeyExtractorClass"),
|
||||
containsString("partitionKeyExpression"),
|
||||
containsString("partitionSelectorClass")));
|
||||
assertThat(e.getMessage(), containsString("for queue:dummy."));
|
||||
}
|
||||
|
||||
bus.unbindProducers("props.0");
|
||||
assertEquals(0, bindings.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestReplyRequestorProperties() throws Exception {
|
||||
MessageBus bus = getMessageBus();
|
||||
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");
|
||||
|
||||
bus.bindRequestor("props.0", new DirectChannel(), new DirectChannel(), properties);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.bindings", List.class);
|
||||
|
||||
assertEquals(2, bindings.size());
|
||||
AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); // producer
|
||||
assertEquals("foo.props.0.requests",
|
||||
TestUtils.getPropertyValue(endpoint, "handler.delegate.routingKey"));
|
||||
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(BusProperties.NEXT_MODULE_COUNT, "1");
|
||||
properties.put("partitionIndex", "0");
|
||||
try {
|
||||
bus.bindRequestor("dummy", null, null, properties);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertThat(e.getMessage(), allOf(
|
||||
containsString("RabbitMessageBus does not support producer properties: "),
|
||||
containsString("partitionSelectorExpression"),
|
||||
containsString("partitionKeyExtractorClass"),
|
||||
containsString("partitionKeyExpression"),
|
||||
containsString("partitionSelectorClass")));
|
||||
assertThat(e.getMessage(), allOf(containsString("partitionIndex"), containsString("for dummy.")));
|
||||
}
|
||||
|
||||
bus.unbindConsumers("props.0");
|
||||
bus.unbindProducers("props.0");
|
||||
assertEquals(0, bindings.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestReplyReplierProperties() throws Exception {
|
||||
MessageBus bus = getMessageBus();
|
||||
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");
|
||||
|
||||
bus.bindReplier("props.0", new DirectChannel(), new DirectChannel(), properties);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.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(BusProperties.NEXT_MODULE_COUNT, "1");
|
||||
properties.put("partitionIndex", "0");
|
||||
try {
|
||||
bus.bindReplier("dummy", null, null, properties);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertThat(e.getMessage(), allOf(
|
||||
containsString("RabbitMessageBus does not support consumer properties: "),
|
||||
containsString("partitionSelectorExpression"),
|
||||
containsString("partitionKeyExtractorClass"),
|
||||
containsString("partitionKeyExpression"),
|
||||
containsString("partitionSelectorClass")));
|
||||
assertThat(e.getMessage(), allOf(containsString("partitionIndex"), containsString("for dummy.")));
|
||||
}
|
||||
|
||||
bus.unbindConsumers("props.0");
|
||||
bus.unbindProducers("props.0");
|
||||
assertEquals(0, bindings.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDurablePubSubWithAutoBindDLQ() throws Exception {
|
||||
RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource());
|
||||
|
||||
MessageBus bus = getMessageBus();
|
||||
Properties properties = new Properties();
|
||||
properties.put("prefix", "xdbustest.");
|
||||
properties.put("autoBindDLQ", "true");
|
||||
properties.put("durableSubscription", "true");
|
||||
properties.put("maxAttempts", "1"); // disable retry
|
||||
properties.put("requeue", "false");
|
||||
DirectChannel moduleInputChannel = new DirectChannel();
|
||||
moduleInputChannel.setBeanName("durableTest");
|
||||
moduleInputChannel.subscribe(new MessageHandler() {
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message<?> message) throws MessagingException {
|
||||
throw new RuntimeException("foo");
|
||||
}
|
||||
|
||||
});
|
||||
bus.bindPubSubConsumer("teststream.tap:stream:durabletest.0", moduleInputChannel, properties);
|
||||
|
||||
RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource());
|
||||
template.convertAndSend("xdbustest.topic.tap:stream:durabletest.0", "", "foo");
|
||||
|
||||
int n = 0;
|
||||
while (n++ < 100) {
|
||||
Object deadLetter = template.receiveAndConvert("xdbustest.teststream.tap:stream:durabletest.0.dlq");
|
||||
if (deadLetter != null) {
|
||||
assertEquals("foo", deadLetter);
|
||||
break;
|
||||
}
|
||||
Thread.sleep(100);
|
||||
}
|
||||
assertTrue(n < 100);
|
||||
|
||||
bus.unbindConsumer("teststream.tap:stream:durabletest.0", moduleInputChannel);
|
||||
assertNotNull(admin.getQueueProperties("xdbustest.teststream.tap:stream:durabletest.0.dlq"));
|
||||
admin.deleteQueue("xdbustest.teststream.tap:stream:durabletest.0.dlq");
|
||||
admin.deleteQueue("xdbustest.teststream.tap:stream:durabletest.0");
|
||||
admin.deleteExchange("xdbustest.topic.tap:stream:durabletest.0");
|
||||
admin.deleteExchange("xdbustest.DLX");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonDurablePubSubWithAutoBindDLQ() throws Exception {
|
||||
RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource());
|
||||
|
||||
MessageBus bus = getMessageBus();
|
||||
Properties properties = new Properties();
|
||||
properties.put("prefix", "xdbustest.");
|
||||
properties.put("autoBindDLQ", "true");
|
||||
properties.put("durableSubscription", "false");
|
||||
properties.put("maxAttempts", "1"); // disable retry
|
||||
properties.put("requeue", "false");
|
||||
DirectChannel moduleInputChannel = new DirectChannel();
|
||||
moduleInputChannel.setBeanName("nondurabletest");
|
||||
moduleInputChannel.subscribe(new MessageHandler() {
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message<?> message) throws MessagingException {
|
||||
throw new RuntimeException("foo");
|
||||
}
|
||||
|
||||
});
|
||||
bus.bindPubSubConsumer("teststream.tap:stream:nondurabletest.0", moduleInputChannel, properties);
|
||||
|
||||
bus.unbindConsumer("teststream.tap:stream:nondurabletest.0", moduleInputChannel);
|
||||
assertNull(admin.getQueueProperties("xdbustest.teststream.tap:stream:nondurabletest.0.dlq"));
|
||||
admin.deleteQueue("xdbustest.teststream.tap:stream:nondurabletest.0");
|
||||
admin.deleteExchange("xdbustest.topic.tap:stream:nondurabletest.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutoBindDLQ() throws Exception {
|
||||
RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource());
|
||||
|
||||
MessageBus bus = getMessageBus();
|
||||
Properties properties = new Properties();
|
||||
properties.put("prefix", "xdbustest.");
|
||||
properties.put("autoBindDLQ", "true");
|
||||
properties.put("maxAttempts", "1"); // disable retry
|
||||
properties.put("requeue", "false");
|
||||
DirectChannel moduleInputChannel = new DirectChannel();
|
||||
moduleInputChannel.setBeanName("dlqTest");
|
||||
moduleInputChannel.subscribe(new MessageHandler() {
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message<?> message) throws MessagingException {
|
||||
throw new RuntimeException("foo");
|
||||
}
|
||||
|
||||
});
|
||||
bus.bindConsumer("dlqtest", moduleInputChannel, properties);
|
||||
|
||||
RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource());
|
||||
template.convertAndSend("", "xdbustest.dlqtest", "foo");
|
||||
|
||||
int n = 0;
|
||||
while (n++ < 100) {
|
||||
Object deadLetter = template.receiveAndConvert("xdbustest.dlqtest.dlq");
|
||||
if (deadLetter != null) {
|
||||
assertEquals("foo", deadLetter);
|
||||
break;
|
||||
}
|
||||
Thread.sleep(100);
|
||||
}
|
||||
assertTrue(n < 100);
|
||||
|
||||
bus.unbindConsumer("dlqtest", moduleInputChannel);
|
||||
admin.deleteQueue("xdbustest.dlqtest.dlq");
|
||||
admin.deleteQueue("xdbustest.dlqtest");
|
||||
admin.deleteExchange("xdbustest.DLX");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutoBindDLQwithRepublish() throws Exception {
|
||||
// pre-declare the queue with dead-lettering, users can also use a policy
|
||||
RabbitAdmin admin = new RabbitAdmin(this.rabbitAvailableRule.getResource());
|
||||
Map<String, Object> args = new HashMap<String, Object>();
|
||||
args.put("x-dead-letter-exchange", "xdbustest.DLX");
|
||||
Queue queue = new Queue("xdbustest.dlqpubtest", true, false, false, args);
|
||||
admin.declareQueue(queue);
|
||||
|
||||
MessageBus bus = getMessageBus();
|
||||
Properties properties = new Properties();
|
||||
properties.put("prefix", "xdbustest.");
|
||||
properties.put("autoBindDLQ", "true");
|
||||
properties.put("republishToDLQ", "true");
|
||||
properties.put("maxAttempts", "1"); // disable retry
|
||||
properties.put("requeue", "false");
|
||||
DirectChannel moduleInputChannel = new DirectChannel();
|
||||
moduleInputChannel.setBeanName("dlqPubTest");
|
||||
moduleInputChannel.subscribe(new MessageHandler() {
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message<?> message) throws MessagingException {
|
||||
throw new RuntimeException("foo");
|
||||
}
|
||||
|
||||
});
|
||||
bus.bindConsumer("dlqpubtest", moduleInputChannel, properties);
|
||||
|
||||
RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource());
|
||||
template.convertAndSend("", "xdbustest.dlqpubtest", "foo");
|
||||
|
||||
int n = 0;
|
||||
while (n++ < 100) {
|
||||
org.springframework.amqp.core.Message deadLetter = template.receive("xdbustest.dlqpubtest.dlq");
|
||||
if (deadLetter != null) {
|
||||
assertEquals("foo", new String(deadLetter.getBody()));
|
||||
assertNotNull(deadLetter.getMessageProperties().getHeaders().get("x-exception-stacktrace"));
|
||||
break;
|
||||
}
|
||||
Thread.sleep(100);
|
||||
}
|
||||
assertTrue(n < 100);
|
||||
|
||||
bus.unbindConsumer("dlqpubtest", moduleInputChannel);
|
||||
admin.deleteQueue("xdbustest.dlqpubtest.dlq");
|
||||
admin.deleteQueue("xdbustest.dlqpubtest");
|
||||
admin.deleteExchange("xdbustest.DLX");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testBatchingAndCompression() throws Exception {
|
||||
RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource());
|
||||
MessageBus bus = getMessageBus();
|
||||
Properties properties = new Properties();
|
||||
properties.put("deliveryMode", "NON_PERSISTENT");
|
||||
properties.put("batchingEnabled", "true");
|
||||
properties.put("batchSize", "2");
|
||||
properties.put("batchBufferLimit", "100000");
|
||||
properties.put("batchTimeout", "30000");
|
||||
properties.put("compress", "true");
|
||||
|
||||
DirectChannel output = new DirectChannel();
|
||||
output.setBeanName("batchingProducer");
|
||||
bus.bindProducer("batching.0", output, properties);
|
||||
|
||||
while (template.receive("xdbus.batching.0") != null) {
|
||||
}
|
||||
|
||||
Log logger = spy(TestUtils.getPropertyValue(bus, "messageBus.compressingPostProcessor.logger", Log.class));
|
||||
new DirectFieldAccessor(TestUtils.getPropertyValue(bus, "messageBus.compressingPostProcessor"))
|
||||
.setPropertyValue("logger", logger);
|
||||
when(logger.isTraceEnabled()).thenReturn(true);
|
||||
|
||||
assertEquals(Deflater.BEST_SPEED, TestUtils.getPropertyValue(bus, "messageBus.compressingPostProcessor.level"));
|
||||
|
||||
output.send(new GenericMessage<>("foo".getBytes()));
|
||||
output.send(new GenericMessage<>("bar".getBytes()));
|
||||
|
||||
Object out = spyOn("batching.0").receive(false);
|
||||
assertThat(out, instanceOf(byte[].class));
|
||||
assertEquals("\u0000\u0000\u0000\u0003foo\u0000\u0000\u0000\u0003bar", new String((byte[]) out));
|
||||
|
||||
ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class);
|
||||
verify(logger).trace(captor.capture());
|
||||
assertThat(captor.getValue().toString(), containsString("Compressed 14 to "));
|
||||
|
||||
QueueChannel input = new QueueChannel();
|
||||
input.setBeanName("batchingConsumer");
|
||||
bus.bindConsumer("batching.0", input, null);
|
||||
|
||||
output.send(new GenericMessage<>("foo".getBytes()));
|
||||
output.send(new GenericMessage<>("bar".getBytes()));
|
||||
|
||||
Message<byte[]> in = (Message<byte[]>) input.receive(10000);
|
||||
assertNotNull(in);
|
||||
assertEquals("foo", new String(in.getPayload()));
|
||||
in = (Message<byte[]>) input.receive(10000);
|
||||
assertNotNull(in);
|
||||
assertEquals("bar", new String(in.getPayload()));
|
||||
assertNull(in.getHeaders().get(AmqpHeaders.DELIVERY_MODE));
|
||||
|
||||
bus.unbindProducers("batching.0");
|
||||
bus.unbindConsumers("batching.0");
|
||||
}
|
||||
|
||||
private SimpleMessageListenerContainer verifyContainer(AbstractEndpoint endpoint) {
|
||||
SimpleMessageListenerContainer container;
|
||||
Advice retry;
|
||||
container = TestUtils.getPropertyValue(endpoint, "messageListenerContainer",
|
||||
SimpleMessageListenerContainer.class);
|
||||
assertEquals(AcknowledgeMode.NONE, container.getAcknowledgeMode());
|
||||
assertThat(container.getQueueNames()[0], startsWith("foo.props.0"));
|
||||
assertFalse(TestUtils.getPropertyValue(container, "transactional", Boolean.class));
|
||||
assertEquals(2, TestUtils.getPropertyValue(container, "concurrentConsumers"));
|
||||
assertEquals(3, TestUtils.getPropertyValue(container, "maxConcurrentConsumers"));
|
||||
assertFalse(TestUtils.getPropertyValue(container, "defaultRequeueRejected", Boolean.class));
|
||||
assertEquals(20, TestUtils.getPropertyValue(container, "prefetchCount"));
|
||||
assertEquals(10, TestUtils.getPropertyValue(container, "txSize"));
|
||||
retry = TestUtils.getPropertyValue(container, "adviceChain", Advice[].class)[0];
|
||||
assertEquals(23, TestUtils.getPropertyValue(retry, "retryOperations.retryPolicy.maxAttempts"));
|
||||
assertEquals(2000L, TestUtils.getPropertyValue(retry, "retryOperations.backOffPolicy.initialInterval"));
|
||||
assertEquals(20000L, TestUtils.getPropertyValue(retry, "retryOperations.backOffPolicy.maxInterval"));
|
||||
assertEquals(5.0, TestUtils.getPropertyValue(retry, "retryOperations.backOffPolicy.multiplier"));
|
||||
|
||||
List<?> requestMatchers = TestUtils.getPropertyValue(endpoint,
|
||||
"headerMapper.requestHeaderMatcher.strategies",
|
||||
List.class);
|
||||
assertEquals(1, requestMatchers.size());
|
||||
assertEquals("foo",
|
||||
TestUtils.getPropertyValue(requestMatchers.get(0), "patterns", Collection.class).iterator().next());
|
||||
|
||||
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",
|
||||
List.class);
|
||||
assertEquals(1, requestMatchers.size());
|
||||
assertEquals("foo",
|
||||
TestUtils.getPropertyValue(requestMatchers.get(0), "patterns", Collection.class).iterator().next());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getEndpointRouting(AbstractEndpoint endpoint) {
|
||||
return TestUtils.getPropertyValue(endpoint, "handler.delegate.routingKeyExpression", SpelExpression.class).getExpressionString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPubSubEndpointRouting(AbstractEndpoint endpoint) {
|
||||
return TestUtils.getPropertyValue(endpoint, "handler.delegate.exchangeNameExpression", SpelExpression.class).getExpressionString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Spy spyOn(final String queue) {
|
||||
final RabbitTemplate template = new RabbitTemplate(this.rabbitAvailableRule.getResource());
|
||||
template.setAfterReceivePostProcessors(new DelegatingDecompressingPostProcessor());
|
||||
return new Spy() {
|
||||
|
||||
@Override
|
||||
public Object receive(boolean expectNull) throws Exception {
|
||||
if (expectNull) {
|
||||
Thread.sleep(50);
|
||||
return template.receiveAndConvert("xdbus." + queue);
|
||||
}
|
||||
Object bar = null;
|
||||
int n = 0;
|
||||
while (n++ < 100 && bar == null) {
|
||||
bar = template.receiveAndConvert("xdbus." + queue);
|
||||
Thread.sleep(100);
|
||||
}
|
||||
assertTrue("Message did not arrive in RabbitMQ", n < 100);
|
||||
return bar;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.rabbit;
|
||||
|
||||
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.core.RabbitAdmin;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.integration.context.IntegrationContextUtils;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.xd.dirt.integration.bus.AbstractTestMessageBus;
|
||||
import org.springframework.xd.dirt.integration.bus.serializer.MultiTypeCodec;
|
||||
|
||||
|
||||
/**
|
||||
* Test support class for {@link org.springframework.xd.dirt.integration.rabbit.RabbitMessageBus}.
|
||||
*
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class RabbitTestMessageBus extends AbstractTestMessageBus<RabbitMessageBus> {
|
||||
|
||||
private final RabbitAdmin rabbitAdmin;
|
||||
|
||||
public RabbitTestMessageBus(ConnectionFactory connectionFactory) {
|
||||
this.rabbitAdmin = new RabbitAdmin(connectionFactory);
|
||||
}
|
||||
|
||||
public RabbitTestMessageBus(ConnectionFactory connectionFactory, MultiTypeCodec<Object> codec) {
|
||||
RabbitMessageBus messageBus = new RabbitMessageBus(connectionFactory, codec);
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.setPoolSize(1);
|
||||
scheduler.afterPropertiesSet();
|
||||
context.getBeanFactory().registerSingleton(IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME, scheduler);
|
||||
context.refresh();
|
||||
messageBus.setApplicationContext(context);
|
||||
this.setMessageBus(messageBus);
|
||||
this.rabbitAdmin = new RabbitAdmin(connectionFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
if (!queues.isEmpty()) {
|
||||
for (String queue : queues) {
|
||||
rabbitAdmin.deleteQueue("xdbus." + queue);
|
||||
// delete any partitioned queues
|
||||
for (int i = 0; i < 10; i++) {
|
||||
rabbitAdmin.deleteQueue("xdbus." + queue + "-" + i);
|
||||
}
|
||||
rabbitAdmin.deleteQueue("foo." + queue);
|
||||
// delete any partitioned queues
|
||||
for (int i = 0; i < 10; i++) {
|
||||
rabbitAdmin.deleteQueue("foo." + queue + "-" + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!topics.isEmpty()) {
|
||||
for (String exchange : topics) {
|
||||
rabbitAdmin.deleteExchange("xdbus." + exchange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2013-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.rabbit;
|
||||
|
||||
|
||||
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
|
||||
import org.springframework.xd.test.AbstractExternalResourceTestSupport;
|
||||
|
||||
import javax.net.SocketFactory;
|
||||
import java.net.Socket;
|
||||
|
||||
/**
|
||||
* JUnit {@link org.junit.Rule} that detects the fact that RabbitMQ is available on localhost.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Gary Russell
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class RabbitTestSupport extends AbstractExternalResourceTestSupport<CachingConnectionFactory> {
|
||||
|
||||
private final boolean management;
|
||||
|
||||
public RabbitTestSupport() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
public RabbitTestSupport(boolean management) {
|
||||
super("RABBIT");
|
||||
this.management = management;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void obtainResource() throws Exception {
|
||||
resource = new CachingConnectionFactory("localhost");
|
||||
resource.createConnection().close();
|
||||
if (management) {
|
||||
Socket socket = SocketFactory.getDefault().createSocket("localhost", 15672);
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanupResource() throws Exception {
|
||||
resource.destroy();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
log4j.rootCategory=WARN, stdout
|
||||
|
||||
# standard logging including calling site
|
||||
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
|
||||
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
|
||||
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %40.40c:%4L - %m%n
|
||||
|
||||
log4j.category.org.springframework.xd.dirt.integration.rabbit=DEBUG
|
||||
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-streams-binding-redis</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>spring-cloud-streams-binding-redis</name>
|
||||
<description>Redis binding implementation</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-bindings-parent</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-spi</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-test</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.integration</groupId>
|
||||
<artifactId>spring-integration-redis</artifactId>
|
||||
<version>${spring-integration.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>joda-time</groupId>
|
||||
<artifactId>joda-time</artifactId>
|
||||
<version>2.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-tuple</artifactId>
|
||||
<version>${spring-xd.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-codec</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,532 @@
|
||||
/*
|
||||
* Copyright 2014-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.redis;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
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;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.SubscribableChannel;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.retry.RecoveryCallback;
|
||||
import org.springframework.retry.RetryCallback;
|
||||
import org.springframework.retry.RetryContext;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.xd.dirt.integration.bus.AbstractBusPropertiesAccessor;
|
||||
import org.springframework.xd.dirt.integration.bus.Binding;
|
||||
import org.springframework.xd.dirt.integration.bus.BusProperties;
|
||||
import org.springframework.xd.dirt.integration.bus.EmbeddedHeadersMessageConverter;
|
||||
import org.springframework.xd.dirt.integration.bus.MessageBus;
|
||||
import org.springframework.xd.dirt.integration.bus.MessageBusSupport;
|
||||
import org.springframework.xd.dirt.integration.bus.MessageValues;
|
||||
import org.springframework.xd.dirt.integration.bus.XdHeaders;
|
||||
import org.springframework.xd.dirt.integration.bus.serializer.MultiTypeCodec;
|
||||
|
||||
/**
|
||||
* A {@link MessageBus} implementation backed by Redis.
|
||||
* @author Mark Fisher
|
||||
* @author Gary Russell
|
||||
* @author David Turanski
|
||||
* @author Jennifer Hickey
|
||||
*/
|
||||
public class RedisMessageBus extends MessageBusSupport implements DisposableBean {
|
||||
|
||||
private static final String ERROR_HEADER = "errorKey";
|
||||
|
||||
private static final SpelExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
private final String[] headersToMap;
|
||||
|
||||
/**
|
||||
* Retry only.
|
||||
*/
|
||||
private static final Set<Object> SUPPORTED_PUBSUB_CONSUMER_PROPERTIES = new SetBuilder()
|
||||
.addAll(CONSUMER_STANDARD_PROPERTIES)
|
||||
.addAll(CONSUMER_RETRY_PROPERTIES)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Retry + concurrency.
|
||||
*/
|
||||
private static final Set<Object> SUPPORTED_NAMED_CONSUMER_PROPERTIES = new SetBuilder()
|
||||
.addAll(CONSUMER_STANDARD_PROPERTIES)
|
||||
.addAll(CONSUMER_RETRY_PROPERTIES)
|
||||
.add(BusProperties.CONCURRENCY)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Named + partitioning.
|
||||
*/
|
||||
private static final Set<Object> SUPPORTED_CONSUMER_PROPERTIES = new SetBuilder()
|
||||
.addAll(SUPPORTED_NAMED_CONSUMER_PROPERTIES)
|
||||
.add(BusProperties.PARTITION_INDEX)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Retry + concurrency (request).
|
||||
*/
|
||||
private static final Set<Object> SUPPORTED_REPLYING_CONSUMER_PROPERTIES = new SetBuilder()
|
||||
// request
|
||||
.addAll(CONSUMER_STANDARD_PROPERTIES)
|
||||
.addAll(CONSUMER_RETRY_PROPERTIES)
|
||||
.add(BusProperties.CONCURRENCY)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* None.
|
||||
*/
|
||||
private static final Set<Object> SUPPORTED_PUBSUB_PRODUCER_PROPERTIES = PRODUCER_STANDARD_PROPERTIES;
|
||||
|
||||
/**
|
||||
* None.
|
||||
*/
|
||||
private static final Set<Object> SUPPORTED_NAMED_PRODUCER_PROPERTIES = PRODUCER_STANDARD_PROPERTIES;
|
||||
|
||||
/**
|
||||
* Partitioning.
|
||||
*/
|
||||
private static final Set<Object> SUPPORTED_PRODUCER_PROPERTIES = new SetBuilder()
|
||||
.addAll(PRODUCER_PARTITIONING_PROPERTIES)
|
||||
.addAll(PRODUCER_STANDARD_PROPERTIES)
|
||||
.add(BusProperties.DIRECT_BINDING_ALLOWED)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Retry, concurrency (reply).
|
||||
*/
|
||||
private static final Set<Object> SUPPORTED_REQUESTING_PRODUCER_PROPERTIES = new SetBuilder()
|
||||
// reply
|
||||
.addAll(CONSUMER_RETRY_PROPERTIES)
|
||||
.add(BusProperties.CONCURRENCY)
|
||||
.build();
|
||||
|
||||
private final RedisConnectionFactory connectionFactory;
|
||||
|
||||
private final EmbeddedHeadersMessageConverter embeddedHeadersMessageConverter = new
|
||||
EmbeddedHeadersMessageConverter();
|
||||
|
||||
private final RedisQueueOutboundChannelAdapter errorAdapter;
|
||||
|
||||
public RedisMessageBus(RedisConnectionFactory connectionFactory, MultiTypeCodec<Object> codec) {
|
||||
this(connectionFactory, codec, new String[0]);
|
||||
}
|
||||
|
||||
public RedisMessageBus(RedisConnectionFactory connectionFactory, MultiTypeCodec<Object> codec,
|
||||
String... headersToMap) {
|
||||
Assert.notNull(connectionFactory, "connectionFactory must not be null");
|
||||
Assert.notNull(codec, "codec must not be null");
|
||||
this.connectionFactory = connectionFactory;
|
||||
setCodec(codec);
|
||||
this.errorAdapter = new RedisQueueOutboundChannelAdapter(
|
||||
parser.parseExpression("headers['" + ERROR_HEADER + "']"), connectionFactory);
|
||||
if (headersToMap != null && headersToMap.length > 0) {
|
||||
String[] combinedHeadersToMap =
|
||||
Arrays.copyOfRange(XdHeaders.STANDARD_HEADERS, 0, XdHeaders.STANDARD_HEADERS.length
|
||||
+ headersToMap.length);
|
||||
System.arraycopy(headersToMap, 0, combinedHeadersToMap, XdHeaders.STANDARD_HEADERS.length,
|
||||
headersToMap.length);
|
||||
this.headersToMap = combinedHeadersToMap;
|
||||
}
|
||||
else {
|
||||
this.headersToMap = XdHeaders.STANDARD_HEADERS;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onInit() {
|
||||
this.errorAdapter.setIntegrationEvaluationContext(this.evaluationContext);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
RedisPropertiesAccessor accessor = new RedisPropertiesAccessor(properties);
|
||||
String queueName = "queue." + name;
|
||||
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);
|
||||
}
|
||||
|
||||
private MessageProducerSupport createInboundAdapter(RedisPropertiesAccessor accessor, String queueName) {
|
||||
MessageProducerSupport adapter;
|
||||
int concurrency = accessor.getConcurrency(this.defaultConcurrency);
|
||||
concurrency = concurrency > 0 ? concurrency : 1;
|
||||
if (concurrency == 1) {
|
||||
RedisQueueMessageDrivenEndpoint single = new RedisQueueMessageDrivenEndpoint(queueName,
|
||||
this.connectionFactory);
|
||||
single.setBeanFactory(getBeanFactory());
|
||||
single.setSerializer(null);
|
||||
adapter = single;
|
||||
}
|
||||
else {
|
||||
adapter = new CompositeRedisQueueMessageDrivenEndpoint(queueName, concurrency);
|
||||
}
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindPubSubConsumer(final String name, MessageChannel moduleInputChannel,
|
||||
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,
|
||||
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.afterPropertiesSet();
|
||||
Binding consumerBinding = Binding.forConsumer(bindingName, adapter, moduleInputChannel, properties);
|
||||
addBinding(consumerBinding);
|
||||
ReceivingHandler convertingBridge = new ReceivingHandler();
|
||||
convertingBridge.setOutputChannel(moduleInputChannel);
|
||||
convertingBridge.setBeanName(channelName + ".bridge.handler");
|
||||
convertingBridge.afterPropertiesSet();
|
||||
bridgeToModuleChannel.subscribe(convertingBridge);
|
||||
consumerBinding.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* If retry is enabled, wrap the bridge channel in another that will invoke send() within the scope of a retry
|
||||
* template.
|
||||
* @param name The name.
|
||||
* @param bridgeToModuleChannel The channel.
|
||||
* @param properties The properties.
|
||||
* @return The channel, or a wrapper.
|
||||
*/
|
||||
private MessageChannel addRetryIfNeeded(final String name, final DirectChannel bridgeToModuleChannel,
|
||||
RedisPropertiesAccessor properties) {
|
||||
final RetryTemplate retryTemplate = buildRetryTemplateIfRetryEnabled(properties);
|
||||
if (retryTemplate == null) {
|
||||
return bridgeToModuleChannel;
|
||||
}
|
||||
else {
|
||||
DirectChannel channel = new DirectChannel() {
|
||||
|
||||
@Override
|
||||
protected boolean doSend(final Message<?> message, final long timeout) {
|
||||
try {
|
||||
return retryTemplate.execute(new RetryCallback<Boolean, Exception>() {
|
||||
|
||||
@Override
|
||||
public Boolean doWithRetry(RetryContext context) throws Exception {
|
||||
return bridgeToModuleChannel.send(message, timeout);
|
||||
}
|
||||
|
||||
}, new RecoveryCallback<Boolean>() {
|
||||
|
||||
/**
|
||||
* Send the failed message to 'ERRORS:[name]'.
|
||||
*/
|
||||
@Override
|
||||
public Boolean recover(RetryContext context) throws Exception {
|
||||
logger.error(
|
||||
"Failed to deliver message; retries exhausted; message sent to queue 'ERRORS:"
|
||||
+ name + "' " + context.getLastThrowable());
|
||||
errorAdapter.handleMessage(getMessageBuilderFactory().fromMessage(message)
|
||||
.setHeader(ERROR_HEADER, "ERRORS:" + name)
|
||||
.build());
|
||||
return true;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
catch (Exception e) {
|
||||
logger.error("Failed to deliver message", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
channel.setBeanName(name + ".bridge");
|
||||
return channel;
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@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) {
|
||||
Assert.isInstanceOf(SubscribableChannel.class, moduleOutputChannel);
|
||||
MessageHandler handler = new SendingHandler(delegate, replyTo, 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);
|
||||
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(
|
||||
RedisMessageBus.parser.parseExpression("headers['" + XdHeaders.REPLY_TO + "']"),
|
||||
this.connectionFactory);
|
||||
replyQueue.setBeanFactory(this.getBeanFactory());
|
||||
replyQueue.setIntegrationEvaluationContext(this.evaluationContext);
|
||||
replyQueue.afterPropertiesSet();
|
||||
this.doRegisterProducer(name, replies, replyQueue, accessor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
stopBindings();
|
||||
}
|
||||
|
||||
private class SendingHandler extends AbstractMessageHandler {
|
||||
|
||||
private final MessageHandler delegate;
|
||||
|
||||
private final String replyTo;
|
||||
|
||||
private final PartitioningMetadata partitioningMetadata;
|
||||
|
||||
|
||||
private SendingHandler(MessageHandler delegate, String replyTo, RedisPropertiesAccessor properties) {
|
||||
this.delegate = delegate;
|
||||
this.replyTo = replyTo;
|
||||
this.partitioningMetadata = new PartitioningMetadata(properties, properties.getNextModuleCount());
|
||||
this.setBeanFactory(RedisMessageBus.this.getBeanFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleMessageInternal(Message<?> message) throws Exception {
|
||||
MessageValues transformed = serializePayloadIfNecessary(message);
|
||||
|
||||
if (replyTo != null) {
|
||||
transformed.put(XdHeaders.REPLY_TO, this.replyTo);
|
||||
}
|
||||
if (this.partitioningMetadata.isPartitionedModule()) {
|
||||
|
||||
transformed.put(PARTITION_HEADER, determinePartition(message, this.partitioningMetadata));
|
||||
}
|
||||
|
||||
byte[] messageToSend = embeddedHeadersMessageConverter.embedHeaders(transformed,
|
||||
RedisMessageBus.this.headersToMap);
|
||||
delegate.handleMessage(MessageBuilder.withPayload(messageToSend).copyHeaders(transformed).build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class ReceivingHandler extends AbstractReplyProducingMessageHandler {
|
||||
|
||||
public ReceivingHandler() {
|
||||
super();
|
||||
this.setBeanFactory(RedisMessageBus.this.getBeanFactory());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
protected Object handleRequestMessage(Message<?> requestMessage) {
|
||||
MessageValues theRequestMessage;
|
||||
try {
|
||||
theRequestMessage = embeddedHeadersMessageConverter.extractHeaders((Message<byte[]>) requestMessage, true);
|
||||
}
|
||||
catch (Exception e) {
|
||||
logger.error(EmbeddedHeadersMessageConverter.decodeExceptionMessage(requestMessage), e);
|
||||
theRequestMessage = new MessageValues(requestMessage);
|
||||
}
|
||||
return deserializePayloadIfNecessary(theRequestMessage).toMessage(getMessageBuilderFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldCopyRequestHeaders() {
|
||||
// prevent returned message from being copied in superclass
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RedisPropertiesAccessor extends AbstractBusPropertiesAccessor {
|
||||
|
||||
public RedisPropertiesAccessor(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides concurrency by creating a list of message-driven endpoints.
|
||||
*/
|
||||
private class CompositeRedisQueueMessageDrivenEndpoint extends MessageProducerSupport {
|
||||
|
||||
private final List<RedisQueueMessageDrivenEndpoint> consumers = new
|
||||
ArrayList<RedisQueueMessageDrivenEndpoint>();
|
||||
|
||||
public CompositeRedisQueueMessageDrivenEndpoint(String queueName, int concurrency) {
|
||||
for (int i = 0; i < concurrency; i++) {
|
||||
RedisQueueMessageDrivenEndpoint adapter = new RedisQueueMessageDrivenEndpoint(queueName,
|
||||
connectionFactory);
|
||||
adapter.setBeanFactory(RedisMessageBus.this.getBeanFactory());
|
||||
adapter.setSerializer(null);
|
||||
adapter.setBeanName("inbound." + queueName + "." + i);
|
||||
this.consumers.add(adapter);
|
||||
}
|
||||
this.setBeanFactory(RedisMessageBus.this.getBeanFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onInit() {
|
||||
for (RedisQueueMessageDrivenEndpoint consumer : consumers) {
|
||||
consumer.afterPropertiesSet();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStart() {
|
||||
for (RedisQueueMessageDrivenEndpoint consumer : consumers) {
|
||||
consumer.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doStop() {
|
||||
for (RedisQueueMessageDrivenEndpoint consumer : consumers) {
|
||||
consumer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOutputChannel(MessageChannel outputChannel) {
|
||||
for (RedisQueueMessageDrivenEndpoint consumer : consumers) {
|
||||
consumer.setOutputChannel(outputChannel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setErrorChannel(MessageChannel errorChannel) {
|
||||
for (RedisQueueMessageDrivenEndpoint consumer : consumers) {
|
||||
consumer.setErrorChannel(errorChannel);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* This package contains an implementation of the {@link org.springframework.xd.dirt.integration.bus.MessageBus} for Redis.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.redis;
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:context="http://www.springframework.org/schema/context"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
|
||||
|
||||
<bean id="messageBus" class="org.springframework.xd.dirt.integration.redis.RedisMessageBus">
|
||||
<constructor-arg ref="redisConnectionFactory" />
|
||||
<constructor-arg ref="codec"/>
|
||||
<constructor-arg value="${xd.messagebus.redis.headers:}" />
|
||||
<property name="defaultBackOffInitialInterval" value="${xd.messagebus.redis.default.backOffInitialInterval}" />
|
||||
<property name="defaultBackOffMaxInterval" value="${xd.messagebus.redis.default.backOffMaxInterval}" />
|
||||
<property name="defaultBackOffMultiplier" value="${xd.messagebus.redis.default.backOffMultiplier}" />
|
||||
<property name="defaultConcurrency" value="${xd.messagebus.redis.default.concurrency}" />
|
||||
<property name="defaultMaxAttempts" value="${xd.messagebus.redis.default.maxAttempts}" />
|
||||
</bean>
|
||||
|
||||
</beans>
|
||||
@@ -0,0 +1,395 @@
|
||||
/*
|
||||
* Copyright 2013-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus.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;
|
||||
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.Properties;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
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.test.util.TestUtils;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.xd.dirt.integration.bus.Binding;
|
||||
import org.springframework.xd.dirt.integration.bus.BusProperties;
|
||||
import org.springframework.xd.dirt.integration.bus.EmbeddedHeadersMessageConverter;
|
||||
import org.springframework.xd.dirt.integration.bus.MessageBus;
|
||||
import org.springframework.xd.dirt.integration.bus.PartitionCapableBusTests;
|
||||
import org.springframework.xd.dirt.integration.bus.Spy;
|
||||
import org.springframework.xd.dirt.integration.redis.RedisMessageBus;
|
||||
import org.springframework.xd.dirt.integration.redis.RedisTestSupport;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class RedisMessageBusTests extends PartitionCapableBusTests {
|
||||
|
||||
@Rule
|
||||
public RedisTestSupport redisAvailableRule = new RedisTestSupport();
|
||||
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private static final EmbeddedHeadersMessageConverter embeddedHeadersMessageConverter =
|
||||
new EmbeddedHeadersMessageConverter();
|
||||
|
||||
@Override
|
||||
protected MessageBus getMessageBus() {
|
||||
if (testMessageBus == null) {
|
||||
testMessageBus = new RedisTestMessageBus(redisAvailableRule.getResource(), getCodec());
|
||||
}
|
||||
return testMessageBus;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean usesExplicitRouting() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testSendAndReceivePubSub() throws Exception {
|
||||
|
||||
TimeUnit.SECONDS.sleep(2); //TODO remove timing issue
|
||||
|
||||
super.testSendAndReceivePubSub();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
createTemplate().boundListOps("queue.direct.0").trim(1, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConsumerProperties() throws Exception {
|
||||
MessageBus bus = getMessageBus();
|
||||
Properties properties = new Properties();
|
||||
properties.put("maxAttempts", "1"); // disable retry
|
||||
bus.bindConsumer("props.0", new DirectChannel(), properties);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.bindings", List.class);
|
||||
assertEquals(1, bindings.size());
|
||||
AbstractEndpoint endpoint = bindings.get(0).getEndpoint();
|
||||
assertThat(endpoint, instanceOf(RedisQueueMessageDrivenEndpoint.class));
|
||||
assertSame(DirectChannel.class, TestUtils.getPropertyValue(endpoint, "outputChannel").getClass());
|
||||
bus.unbindConsumers("props.0");
|
||||
assertEquals(0, bindings.size());
|
||||
|
||||
properties.put("backOffInitialInterval", "2000");
|
||||
properties.put("backOffMaxInterval", "20000");
|
||||
properties.put("backOffMultiplier", "5.0");
|
||||
properties.put("concurrency", "2");
|
||||
properties.put("maxAttempts", "23");
|
||||
properties.put("partitionIndex", 0);
|
||||
|
||||
bus.bindConsumer("props.0", new DirectChannel(), properties);
|
||||
assertEquals(1, bindings.size());
|
||||
endpoint = bindings.get(0).getEndpoint();
|
||||
verifyConsumer(endpoint);
|
||||
|
||||
try {
|
||||
bus.bindPubSubConsumer("dummy", null, properties);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertThat(e.getMessage(), allOf(
|
||||
containsString("RedisMessageBus does not support consumer properties: "),
|
||||
containsString("partitionIndex"),
|
||||
containsString("concurrency"),
|
||||
containsString(" for dummy.")));
|
||||
}
|
||||
try {
|
||||
bus.bindConsumer("queue:dummy", null, properties);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertEquals("RedisMessageBus does not support consumer property: partitionIndex for queue:dummy.",
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
bus.unbindConsumers("props.0");
|
||||
assertEquals(0, bindings.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProducerProperties() throws Exception {
|
||||
MessageBus bus = getMessageBus();
|
||||
bus.bindProducer("props.0", new DirectChannel(), null);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.bindings", List.class);
|
||||
assertEquals(1, bindings.size());
|
||||
AbstractEndpoint endpoint = bindings.get(0).getEndpoint();
|
||||
assertEquals(
|
||||
"queue.props.0",
|
||||
TestUtils.getPropertyValue(endpoint, "handler.delegate.queueNameExpression", Expression.class).getExpressionString());
|
||||
bus.unbindProducers("props.0");
|
||||
assertEquals(0, bindings.size());
|
||||
|
||||
Properties properties = new Properties();
|
||||
properties.put("partitionKeyExpression", "'foo'");
|
||||
properties.put("partitionKeyExtractorClass", "foo");
|
||||
properties.put("partitionSelectorExpression", "0");
|
||||
properties.put("partitionSelectorClass", "foo");
|
||||
properties.put(BusProperties.NEXT_MODULE_COUNT, "1");
|
||||
|
||||
bus.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 {
|
||||
bus.bindPubSubProducer("dummy", null, properties);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertThat(e.getMessage(), allOf(
|
||||
containsString("RedisMessageBus does not support producer properties: "),
|
||||
containsString("partitionSelectorExpression"),
|
||||
containsString("partitionKeyExtractorClass"),
|
||||
containsString("partitionKeyExpression"),
|
||||
containsString("partitionSelectorClass")));
|
||||
assertThat(e.getMessage(), containsString("for dummy."));
|
||||
}
|
||||
try {
|
||||
bus.bindProducer("queue:dummy", new DirectChannel(), properties);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertThat(e.getMessage(), allOf(
|
||||
containsString("RedisMessageBus does not support producer properties: "),
|
||||
containsString("partitionSelectorExpression"),
|
||||
containsString("partitionKeyExtractorClass"),
|
||||
containsString("partitionKeyExpression"),
|
||||
containsString("partitionSelectorClass")));
|
||||
assertThat(e.getMessage(), containsString("for queue:dummy."));
|
||||
}
|
||||
|
||||
bus.unbindProducers("props.0");
|
||||
assertEquals(0, bindings.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestReplyRequestorProperties() throws Exception {
|
||||
MessageBus bus = getMessageBus();
|
||||
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");
|
||||
|
||||
bus.bindRequestor("props.0", new DirectChannel(), new DirectChannel(), properties);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.bindings", List.class);
|
||||
|
||||
assertEquals(2, bindings.size());
|
||||
AbstractEndpoint endpoint = bindings.get(0).getEndpoint(); // producer
|
||||
assertEquals(
|
||||
"queue.props.0.requests",
|
||||
TestUtils.getPropertyValue(endpoint, "handler.delegate.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 {
|
||||
bus.bindRequestor("dummy", new DirectChannel(), new DirectChannel(), properties);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertThat(e.getMessage(), allOf(
|
||||
containsString("RedisMessageBus does not support producer properties: "),
|
||||
containsString("partitionSelectorExpression"),
|
||||
containsString("partitionKeyExtractorClass"),
|
||||
containsString("partitionKeyExpression"),
|
||||
containsString("partitionSelectorClass")));
|
||||
assertThat(e.getMessage(), allOf(containsString("partitionIndex"), containsString("for dummy.")));
|
||||
}
|
||||
|
||||
bus.unbindConsumers("props.0");
|
||||
bus.unbindProducers("props.0");
|
||||
assertEquals(0, bindings.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequestReplyReplierProperties() throws Exception {
|
||||
MessageBus bus = getMessageBus();
|
||||
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");
|
||||
|
||||
bus.bindReplier("props.0", new DirectChannel(), new DirectChannel(), properties);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.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(BusProperties.NEXT_MODULE_COUNT, "1");
|
||||
properties.put("partitionIndex", "0");
|
||||
try {
|
||||
bus.bindReplier("dummy", new DirectChannel(), new DirectChannel(), properties);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertThat(e.getMessage(), allOf(
|
||||
containsString("RedisMessageBus does not support consumer properties: "),
|
||||
containsString("partitionSelectorExpression"),
|
||||
containsString("partitionKeyExtractorClass"),
|
||||
containsString("partitionKeyExpression"),
|
||||
containsString("partitionSelectorClass")));
|
||||
assertThat(e.getMessage(), allOf(containsString("partitionIndex"), containsString("for dummy.")));
|
||||
}
|
||||
|
||||
bus.unbindConsumers("props.0");
|
||||
bus.unbindProducers("props.0");
|
||||
assertEquals(0, bindings.size());
|
||||
}
|
||||
|
||||
private void verifyConsumer(AbstractEndpoint endpoint) {
|
||||
assertThat(endpoint.getClass().getName(), containsString("CompositeRedisQueueMessageDrivenEndpoint"));
|
||||
assertEquals(2, TestUtils.getPropertyValue(endpoint, "consumers", Collection.class).size());
|
||||
DirectChannel channel = TestUtils.getPropertyValue(
|
||||
TestUtils.getPropertyValue(endpoint, "consumers", List.class).get(0),
|
||||
"outputChannel", DirectChannel.class);
|
||||
assertThat(
|
||||
channel.getClass().getName(), containsString("RedisMessageBus$")); // retry wrapper
|
||||
assertThat(
|
||||
TestUtils.getPropertyValue(TestUtils.getPropertyValue(endpoint, "consumers", List.class).get(1),
|
||||
"outputChannel").getClass().getName(), containsString("RedisMessageBus$")); // retry wrapper
|
||||
RetryTemplate retry = TestUtils.getPropertyValue(channel, "val$retryTemplate", RetryTemplate.class);
|
||||
assertEquals(23, TestUtils.getPropertyValue(retry, "retryPolicy.maxAttempts"));
|
||||
assertEquals(2000L, TestUtils.getPropertyValue(retry, "backOffPolicy.initialInterval"));
|
||||
assertEquals(20000L, TestUtils.getPropertyValue(retry, "backOffPolicy.maxInterval"));
|
||||
assertEquals(5.0, TestUtils.getPropertyValue(retry, "backOffPolicy.multiplier"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRetryFail() {
|
||||
MessageBus bus = getMessageBus();
|
||||
DirectChannel channel = new DirectChannel();
|
||||
bus.bindProducer("retry.0", channel, null);
|
||||
Properties props = new Properties();
|
||||
props.put("maxAttempts", 2);
|
||||
props.put("backOffInitialInterval", 100);
|
||||
props.put("backOffMultiplier", "1.0");
|
||||
bus.bindConsumer("retry.0", new DirectChannel(), props); // no subscriber
|
||||
channel.send(new GenericMessage<String>("foo"));
|
||||
RedisTemplate<String, Object> template = createTemplate();
|
||||
Object rightPop = template.boundListOps("ERRORS:retry.0").rightPop(5, TimeUnit.SECONDS);
|
||||
assertNotNull(rightPop);
|
||||
assertThat(new String((byte[]) rightPop), containsString("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMoreHeaders() {
|
||||
RedisMessageBus bus = new RedisMessageBus(mock(RedisConnectionFactory.class), getCodec(), "foo", "bar");
|
||||
Collection<String> headers = Arrays.asList(TestUtils.getPropertyValue(bus, "headersToMap", String[].class));
|
||||
assertEquals(10, headers.size());
|
||||
assertTrue(headers.contains("foo"));
|
||||
assertTrue(headers.contains("bar"));
|
||||
}
|
||||
|
||||
private RedisTemplate<String, Object> createTemplate() {
|
||||
if (this.redisTemplate != null) {
|
||||
return this.redisTemplate;
|
||||
}
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
|
||||
template.setConnectionFactory(this.redisAvailableRule.getResource());
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setEnableDefaultSerializer(false);
|
||||
template.afterPropertiesSet();
|
||||
this.redisTemplate = template;
|
||||
return template;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getEndpointRouting(AbstractEndpoint endpoint) {
|
||||
return TestUtils.getPropertyValue(endpoint, "handler.delegate.queueNameExpression", Expression.class).getExpressionString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPubSubEndpointRouting(AbstractEndpoint endpoint) {
|
||||
return TestUtils.getPropertyValue(endpoint, "handler.delegate.topicExpression", Expression.class).getExpressionString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Spy spyOn(final String queue) {
|
||||
final RedisTemplate<String, Object> template = createTemplate();
|
||||
return new Spy() {
|
||||
|
||||
@Override
|
||||
public Object receive(boolean expectNull) throws Exception {
|
||||
byte[] bytes = (byte[]) template.boundListOps("queue." + queue).rightPop(50, TimeUnit.MILLISECONDS);
|
||||
if (bytes == null) {
|
||||
return null;
|
||||
}
|
||||
bytes = (byte[]) embeddedHeadersMessageConverter.extractHeaders(new GenericMessage<byte[]>(bytes), false).getPayload();
|
||||
return new String(bytes, "UTF-8");
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void busBindUnbindLatency() throws InterruptedException {
|
||||
Thread.sleep(3000); // needed for Redis see INT-3442
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus.redis;
|
||||
|
||||
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.context.IntegrationContextUtils;
|
||||
import org.springframework.integration.support.DefaultMessageBuilderFactory;
|
||||
import org.springframework.integration.support.utils.IntegrationUtils;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.xd.dirt.integration.bus.AbstractTestMessageBus;
|
||||
import org.springframework.xd.dirt.integration.bus.serializer.MultiTypeCodec;
|
||||
import org.springframework.xd.dirt.integration.redis.RedisMessageBus;
|
||||
|
||||
|
||||
/**
|
||||
* Test support class for {@link RedisMessageBus}.
|
||||
*
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class RedisTestMessageBus extends AbstractTestMessageBus<RedisMessageBus> {
|
||||
|
||||
private StringRedisTemplate template;
|
||||
|
||||
public RedisTestMessageBus(RedisConnectionFactory connectionFactory) {
|
||||
template = new StringRedisTemplate(connectionFactory);
|
||||
}
|
||||
|
||||
public RedisTestMessageBus(RedisConnectionFactory connectionFactory, MultiTypeCodec<Object> codec) {
|
||||
RedisMessageBus messageBus = new RedisMessageBus(connectionFactory, codec);
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
context.getBeanFactory().registerSingleton(IntegrationUtils.INTEGRATION_MESSAGE_BUILDER_FACTORY_BEAN_NAME,
|
||||
new DefaultMessageBuilderFactory());
|
||||
DefaultHeaderChannelRegistry channelRegistry = new DefaultHeaderChannelRegistry();
|
||||
channelRegistry.setReaperDelay(Long.MAX_VALUE);
|
||||
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
|
||||
taskScheduler.afterPropertiesSet();
|
||||
channelRegistry.setTaskScheduler(taskScheduler);
|
||||
context.getBeanFactory().registerSingleton(
|
||||
IntegrationContextUtils.INTEGRATION_HEADER_CHANNEL_REGISTRY_BEAN_NAME,
|
||||
channelRegistry);
|
||||
context.refresh();
|
||||
messageBus.setApplicationContext(context);
|
||||
setMessageBus(messageBus);
|
||||
template = new StringRedisTemplate(connectionFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
if (!queues.isEmpty()) {
|
||||
for (String queue : queues) {
|
||||
template.delete(queue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.redis;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.xd.tuple.Tuple;
|
||||
import org.springframework.xd.tuple.TupleBuilder;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* @author David Turanski
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractRedisSerializerTests {
|
||||
|
||||
private RedisSerializer<Object> serializer;
|
||||
|
||||
protected abstract RedisSerializer<Object> getSerializer();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
serializer = getSerializer();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRandomObjectSerialization() {
|
||||
Foo foo = new Foo("hello");
|
||||
byte[] bytes = serializer.serialize(foo);
|
||||
Object obj = serializer.deserialize(bytes);
|
||||
assertTrue(obj instanceof Foo);
|
||||
assertEquals("hello", ((Foo) obj).bar);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDateSerialization() {
|
||||
Date d = new Date();
|
||||
byte[] bytes = serializer.serialize(d);
|
||||
Date obj = (Date) serializer.deserialize(bytes);
|
||||
assertEquals(d, obj);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDateTimeSerialization() {
|
||||
DateTime d = new DateTime();
|
||||
byte[] bytes = serializer.serialize(d);
|
||||
DateTime obj = (DateTime) serializer.deserialize(bytes);
|
||||
assertEquals(d, obj);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStringSerialization() {
|
||||
String s = new String("hello");
|
||||
byte[] bytes = serializer.serialize(s);
|
||||
Object obj = serializer.deserialize(bytes);
|
||||
assertEquals(s, obj);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongSerialization() {
|
||||
byte[] bytes = serializer.serialize(100L);
|
||||
long obj = (Long) serializer.deserialize(bytes);
|
||||
assertEquals(100, obj);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFloatSerialization() {
|
||||
byte[] bytes = serializer.serialize(99.9f);
|
||||
double obj = (Double) serializer.deserialize(bytes);
|
||||
assertEquals(99.9, obj, 0.1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBooleanSerialization() {
|
||||
byte[] bytes = serializer.serialize(true);
|
||||
boolean obj = (Boolean) serializer.deserialize(bytes);
|
||||
assertTrue(obj);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMapSerialization() {
|
||||
Map<String, String> map = new HashMap<String, String>();
|
||||
map.put("foo", "bar");
|
||||
byte[] bytes = serializer.serialize(map);
|
||||
Map<?, ?> obj = (Map<?, ?>) serializer.deserialize(bytes);
|
||||
assertEquals("bar", obj.get("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListSerialization() {
|
||||
List<String> list = new LinkedList<String>();
|
||||
list.add("foo");
|
||||
byte[] bytes = serializer.serialize(list);
|
||||
List<?> obj = (List<?>) serializer.deserialize(bytes);
|
||||
assertEquals("foo", obj.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetSerialization() {
|
||||
Set<String> set = new TreeSet<String>();
|
||||
set.add("foo");
|
||||
byte[] bytes = serializer.serialize(set);
|
||||
Set<?> obj = (Set<?>) serializer.deserialize(bytes);
|
||||
assertEquals("foo", obj.iterator().next());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTupleSerialization() {
|
||||
Tuple t = TupleBuilder.tuple().of("foo", "bar");
|
||||
byte[] bytes = serializer.serialize(t);
|
||||
|
||||
Tuple obj = (Tuple) serializer.deserialize(bytes);
|
||||
assertEquals("bar", obj.getString("foo"));
|
||||
}
|
||||
|
||||
public static class Foo {
|
||||
|
||||
@JsonCreator
|
||||
public Foo(@JsonProperty("bar") String val) {
|
||||
bar = val;
|
||||
}
|
||||
|
||||
public String bar;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.redis;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.listener.ChannelTopic;
|
||||
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||
import org.springframework.data.redis.listener.Topic;
|
||||
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
|
||||
import org.springframework.data.redis.serializer.GenericToStringSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.integration.redis.outbound.RedisPublishingMessageHandler;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.xd.dirt.integration.bus.BusTestUtils;
|
||||
|
||||
/**
|
||||
* Temporary copy of SI RedisPublishingMessageHandlerTests that adds tests that publish messages with data types other
|
||||
* than String
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Jennifer Hickey
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class RedisPublishingMessageHandlerTests {
|
||||
|
||||
private static final String TOPIC = "si.test.channel";
|
||||
|
||||
private static final int NUM_MESSAGES = 10;
|
||||
|
||||
private RedisConnectionFactory connectionFactory;
|
||||
|
||||
private RedisMessageListenerContainer container;
|
||||
|
||||
private CountDownLatch latch = new CountDownLatch(NUM_MESSAGES);
|
||||
|
||||
@Rule
|
||||
public RedisTestSupport redisAvailableRule = new RedisTestSupport();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.connectionFactory = redisAvailableRule.getResource();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithDefaultSerializer() throws Exception {
|
||||
setupListener(new StringRedisSerializer());
|
||||
final RedisPublishingMessageHandler handler = new RedisPublishingMessageHandler(connectionFactory);
|
||||
handler.setBeanFactory(BusTestUtils.MOCK_BF);
|
||||
handler.setTopic(TOPIC);
|
||||
handler.afterPropertiesSet();
|
||||
for (int i = 0; i < NUM_MESSAGES; i++) {
|
||||
handler.handleMessage(MessageBuilder.withPayload("test-" + i).build());
|
||||
}
|
||||
latch.await(3, TimeUnit.SECONDS);
|
||||
assertEquals(0, latch.getCount());
|
||||
container.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithNoSerializer() throws Exception {
|
||||
setupListener(null);
|
||||
final RedisPublishingMessageHandler handler = new RedisPublishingMessageHandler(connectionFactory);
|
||||
handler.setBeanFactory(BusTestUtils.MOCK_BF);
|
||||
handler.setTopic(TOPIC);
|
||||
handler.afterPropertiesSet();
|
||||
for (int i = 0; i < NUM_MESSAGES; i++) {
|
||||
handler.handleMessage(MessageBuilder.withPayload(new String("test-" + i).getBytes()).build());
|
||||
}
|
||||
latch.await(3, TimeUnit.SECONDS);
|
||||
assertEquals(0, latch.getCount());
|
||||
container.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithCustomSerializer() throws Exception {
|
||||
GenericToStringSerializer<Long> serializer = new GenericToStringSerializer<Long>(Long.class);
|
||||
setupListener(serializer);
|
||||
final RedisPublishingMessageHandler handler = new RedisPublishingMessageHandler(connectionFactory);
|
||||
handler.setBeanFactory(BusTestUtils.MOCK_BF);
|
||||
handler.setTopic(TOPIC);
|
||||
handler.setSerializer(serializer);
|
||||
handler.afterPropertiesSet();
|
||||
for (long i = 0; i < NUM_MESSAGES; i++) {
|
||||
handler.handleMessage(MessageBuilder.withPayload(i).build());
|
||||
}
|
||||
latch.await(3, TimeUnit.SECONDS);
|
||||
assertEquals(0, latch.getCount());
|
||||
container.stop();
|
||||
}
|
||||
|
||||
private void setupListener(RedisSerializer<?> listenerSerializer) throws InterruptedException {
|
||||
MessageListenerAdapter listener = new MessageListenerAdapter();
|
||||
listener.setDelegate(new Listener(latch));
|
||||
listener.setSerializer(listenerSerializer);
|
||||
listener.afterPropertiesSet();
|
||||
|
||||
this.container = new RedisMessageListenerContainer();
|
||||
container.setConnectionFactory(connectionFactory);
|
||||
container.afterPropertiesSet();
|
||||
container.addMessageListener(listener, Collections.<Topic> singletonList(new ChannelTopic(TOPIC)));
|
||||
container.start();
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
|
||||
private static class Listener {
|
||||
|
||||
private final CountDownLatch latch;
|
||||
|
||||
private Listener(CountDownLatch latch) {
|
||||
this.latch = latch;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void handleMessage(Object s) {
|
||||
this.latch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.redis;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.BlockingDeque;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericToStringSerializer;
|
||||
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.SerializationException;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.redis.inbound.RedisQueueMessageDrivenEndpoint;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.xd.dirt.integration.bus.BusTestUtils;
|
||||
|
||||
/**
|
||||
* Integration test of {@link RedisQueueInboundChannelAdapter}
|
||||
*
|
||||
* @author Jennifer Hickey
|
||||
*/
|
||||
public class RedisQueueInboundChannelAdapterTests {
|
||||
|
||||
private static final String QUEUE_NAME = "inboundadaptertest";
|
||||
|
||||
private RedisConnectionFactory connectionFactory;
|
||||
|
||||
private final BlockingDeque<Object> messages = new LinkedBlockingDeque<Object>(99);
|
||||
|
||||
private RedisQueueMessageDrivenEndpoint adapter;
|
||||
|
||||
@Rule
|
||||
public RedisTestSupport redisAvailableRule = new RedisTestSupport();
|
||||
|
||||
private String currentQueueName;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
messages.clear();
|
||||
this.connectionFactory = redisAvailableRule.getResource();
|
||||
DirectChannel outputChannel = new DirectChannel();
|
||||
outputChannel.setBeanFactory(BusTestUtils.MOCK_BF);
|
||||
outputChannel.subscribe(new TestMessageHandler());
|
||||
|
||||
this.currentQueueName = QUEUE_NAME + ":" + System.nanoTime();
|
||||
|
||||
adapter = new RedisQueueMessageDrivenEndpoint(currentQueueName, connectionFactory);
|
||||
adapter.setBeanFactory(BusTestUtils.MOCK_BF);
|
||||
adapter.setOutputChannel(outputChannel);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
adapter.stop();
|
||||
connectionFactory.getConnection().del(currentQueueName.getBytes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultPayloadSerializer() throws Exception {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.afterPropertiesSet();
|
||||
|
||||
adapter.afterPropertiesSet();
|
||||
adapter.start();
|
||||
|
||||
template.boundListOps(currentQueueName).rightPush("message1");
|
||||
@SuppressWarnings("unchecked")
|
||||
Message<String> message = (Message<String>) messages.poll(1, TimeUnit.SECONDS);
|
||||
assertNotNull(message);
|
||||
assertEquals("message1", message.getPayload());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultMsgSerializer() throws Exception {
|
||||
RedisTemplate<String, Message<String>> template = new RedisTemplate<String, Message<String>>();
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(new JdkSerializationRedisSerializer());
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.afterPropertiesSet();
|
||||
|
||||
adapter.setExpectMessage(true);
|
||||
adapter.afterPropertiesSet();
|
||||
adapter.start();
|
||||
|
||||
Map<String, Object> headers = new HashMap<String, Object>();
|
||||
headers.put("header1", "foo");
|
||||
template.boundListOps(currentQueueName).rightPush(new GenericMessage<String>("message2", headers));
|
||||
@SuppressWarnings("unchecked")
|
||||
Message<String> message = (Message<String>) messages.poll(1, TimeUnit.SECONDS);
|
||||
assertEquals("message2", message.getPayload());
|
||||
assertEquals("foo", message.getHeaders().get("header1"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testNoSerializer() throws Exception {
|
||||
RedisTemplate<String, byte[]> template = new RedisTemplate<String, byte[]>();
|
||||
template.setEnableDefaultSerializer(false);
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.afterPropertiesSet();
|
||||
|
||||
adapter.setSerializer(null);
|
||||
adapter.afterPropertiesSet();
|
||||
adapter.start();
|
||||
|
||||
template.boundListOps(currentQueueName).rightPush("message3".getBytes());
|
||||
Message<byte[]> message = (Message<byte[]>) messages.poll(1, TimeUnit.SECONDS);
|
||||
assertEquals("message3", new String(message.getPayload()));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testNoSerializerNoExtractPayload() throws Exception {
|
||||
RedisTemplate<String, byte[]> template = new RedisTemplate<String, byte[]>();
|
||||
template.setEnableDefaultSerializer(false);
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.afterPropertiesSet();
|
||||
|
||||
adapter.setSerializer(null);
|
||||
adapter.setExpectMessage(true);
|
||||
adapter.afterPropertiesSet();
|
||||
adapter.start();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomPayloadSerializer() throws Exception {
|
||||
RedisTemplate<String, Long> template = new RedisTemplate<String, Long>();
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(new GenericToStringSerializer<Long>(Long.class));
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.afterPropertiesSet();
|
||||
|
||||
adapter.setSerializer(new GenericToStringSerializer<Long>(Long.class));
|
||||
adapter.afterPropertiesSet();
|
||||
adapter.start();
|
||||
|
||||
template.boundListOps(currentQueueName).rightPush(5l);
|
||||
@SuppressWarnings("unchecked")
|
||||
Message<Long> message = (Message<Long>) messages.poll(1, TimeUnit.SECONDS);
|
||||
assertEquals(5L, (long) message.getPayload());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomMessageSerializer() throws Exception {
|
||||
RedisTemplate<String, Message<?>> template = new RedisTemplate<String, Message<?>>();
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(new TestMessageSerializer());
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.afterPropertiesSet();
|
||||
|
||||
adapter.setSerializer(new TestMessageSerializer());
|
||||
adapter.setExpectMessage(true);
|
||||
adapter.afterPropertiesSet();
|
||||
adapter.start();
|
||||
|
||||
template.boundListOps(currentQueueName).rightPush(new GenericMessage<Long>(10l));
|
||||
@SuppressWarnings("unchecked")
|
||||
Message<Long> message = (Message<Long>) messages.poll(1, TimeUnit.SECONDS);
|
||||
assertEquals(10L, (long) message.getPayload());
|
||||
}
|
||||
|
||||
private class TestMessageHandler implements MessageHandler {
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message<?> message) throws MessagingException {
|
||||
messages.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestMessageSerializer implements RedisSerializer<Message<?>> {
|
||||
|
||||
@Override
|
||||
public byte[] serialize(Message<?> t) throws SerializationException {
|
||||
return "Foo".getBytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> deserialize(byte[] bytes) throws SerializationException {
|
||||
return new GenericMessage<Long>(10l);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.redis;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericToStringSerializer;
|
||||
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.SerializationException;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.integration.redis.outbound.RedisQueueOutboundChannelAdapter;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.xd.dirt.integration.bus.BusTestUtils;
|
||||
|
||||
/**
|
||||
* Integration test of {@link RedisQueueOutboundChannelAdapter}
|
||||
*
|
||||
* @author Jennifer Hickey
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class RedisQueueOutboundChannelAdapterTests {
|
||||
|
||||
private static final String QUEUE_NAME = "outboundadaptertest";
|
||||
|
||||
private RedisConnectionFactory connectionFactory;
|
||||
|
||||
private RedisQueueOutboundChannelAdapter adapter;
|
||||
|
||||
@Rule
|
||||
public RedisTestSupport redisAvailableRule = new RedisTestSupport();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.connectionFactory = redisAvailableRule.getResource();
|
||||
adapter = new RedisQueueOutboundChannelAdapter(QUEUE_NAME, connectionFactory);
|
||||
adapter.setBeanFactory(BusTestUtils.MOCK_BF);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
connectionFactory.getConnection().del(QUEUE_NAME.getBytes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultPayloadSerializer() throws Exception {
|
||||
StringRedisTemplate template = new StringRedisTemplate(connectionFactory);
|
||||
template.afterPropertiesSet();
|
||||
|
||||
adapter.afterPropertiesSet();
|
||||
adapter.handleMessage(new GenericMessage<String>("message1"));
|
||||
assertEquals("message1", template.boundListOps(QUEUE_NAME).rightPop());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultMsgSerializer() throws Exception {
|
||||
RedisTemplate<String, Message<?>> template = new RedisTemplate<String, Message<?>>();
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(new JdkSerializationRedisSerializer());
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.afterPropertiesSet();
|
||||
|
||||
adapter.setExtractPayload(false);
|
||||
adapter.afterPropertiesSet();
|
||||
|
||||
Map<String, Object> headers = new HashMap<String, Object>();
|
||||
headers.put("header1", "foo");
|
||||
adapter.handleMessage(new GenericMessage<String>("message2", headers));
|
||||
Message<?> message = template.boundListOps(QUEUE_NAME).rightPop();
|
||||
assertEquals("message2", message.getPayload());
|
||||
assertEquals("foo", message.getHeaders().get("header1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoSerializer() throws Exception {
|
||||
RedisTemplate<String, byte[]> template = new RedisTemplate<String, byte[]>();
|
||||
template.setEnableDefaultSerializer(false);
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.afterPropertiesSet();
|
||||
|
||||
adapter.afterPropertiesSet();
|
||||
|
||||
adapter.handleMessage(new GenericMessage<byte[]>("message3".getBytes()));
|
||||
byte[] value = template.boundListOps(QUEUE_NAME).rightPop();
|
||||
assertEquals("message3", new String(value));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testNoSerializerNoExtractPayload() throws Exception {
|
||||
RedisTemplate<String, byte[]> template = new RedisTemplate<String, byte[]>();
|
||||
template.setEnableDefaultSerializer(false);
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.afterPropertiesSet();
|
||||
|
||||
adapter.setSerializer(null);
|
||||
adapter.setExtractPayload(false);
|
||||
adapter.afterPropertiesSet();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomPayloadSerializer() throws Exception {
|
||||
RedisTemplate<String, Long> template = new RedisTemplate<String, Long>();
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(new GenericToStringSerializer<Long>(Long.class));
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.afterPropertiesSet();
|
||||
|
||||
adapter.setSerializer(new GenericToStringSerializer<Long>(Long.class));
|
||||
adapter.afterPropertiesSet();
|
||||
|
||||
adapter.handleMessage(new GenericMessage<Long>(5l));
|
||||
assertEquals(Long.valueOf(5), template.boundListOps(QUEUE_NAME).rightPop());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomMessageSerializer() throws Exception {
|
||||
RedisTemplate<String, Message<?>> template = new RedisTemplate<String, Message<?>>();
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(new TestMessageSerializer());
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.afterPropertiesSet();
|
||||
|
||||
adapter.setSerializer(new TestMessageSerializer());
|
||||
adapter.setExtractPayload(false);
|
||||
adapter.afterPropertiesSet();
|
||||
|
||||
Message<?> message = template.boundListOps(QUEUE_NAME).rightPop();
|
||||
assertEquals(10l, message.getPayload());
|
||||
}
|
||||
|
||||
private class TestMessageSerializer implements RedisSerializer<Message<?>> {
|
||||
|
||||
@Override
|
||||
public byte[] serialize(Message<?> t) throws SerializationException {
|
||||
return "Foo".getBytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> deserialize(byte[] bytes) throws SerializationException {
|
||||
return new GenericMessage<Long>(10l);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.redis;
|
||||
|
||||
import org.junit.Rule;
|
||||
|
||||
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
|
||||
import org.springframework.xd.test.AbstractExternalResourceTestSupport;
|
||||
|
||||
/**
|
||||
* JUnit {@link Rule} that detects the fact that a Redis server is running on localhost.
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class RedisTestSupport extends AbstractExternalResourceTestSupport<JedisConnectionFactory> {
|
||||
|
||||
public RedisTestSupport() {
|
||||
super("REDIS");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void obtainResource() throws Exception {
|
||||
resource = new JedisConnectionFactory();
|
||||
resource.afterPropertiesSet();
|
||||
resource.getConnection().close();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanupResource() throws Exception {
|
||||
resource.destroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>spring-cloud-streams-binding-spi</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>spring-cloud-streams-binding-spi</name>
|
||||
<description>SPI for binding implementations</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-bindings-parent</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-codec</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
<version>${spring-framework.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.retry</groupId>
|
||||
<artifactId>spring-retry</artifactId>
|
||||
<version>1.1.0.RELEASE</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>2.4.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.3.6</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,379 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
|
||||
/**
|
||||
* Base class for bus-specific property accessors; common properties
|
||||
* are defined here.
|
||||
*
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public abstract class AbstractBusPropertiesAccessor implements BusProperties {
|
||||
|
||||
private static final SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
|
||||
|
||||
private final Properties properties;
|
||||
|
||||
public AbstractBusPropertiesAccessor(Properties properties) {
|
||||
if (properties == null) {
|
||||
this.properties = new Properties();
|
||||
}
|
||||
else {
|
||||
this.properties = properties;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the underlying properties object.
|
||||
* @return The properties.
|
||||
*/
|
||||
public Properties getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the property for the key, or null if it doesn't exist.
|
||||
* @param key The property.
|
||||
* @return The key.
|
||||
*/
|
||||
public String getProperty(String key) {
|
||||
return this.properties.getProperty(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the property for the key, or the default value if the
|
||||
* property doesn't exist.
|
||||
* @param key The key.
|
||||
* @param defaultValue The default value.
|
||||
* @return The property or default value.
|
||||
*/
|
||||
public String getProperty(String key, String defaultValue) {
|
||||
return this.properties.getProperty(key, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the property for the key, or the default value if the
|
||||
* property doesn't exist.
|
||||
* @param key The key.
|
||||
* @param defaultValue The default value.
|
||||
* @return The property or default value.
|
||||
*/
|
||||
public boolean getProperty(String key, boolean defaultValue) {
|
||||
String property = this.properties.getProperty(key);
|
||||
if (property != null) {
|
||||
return Boolean.parseBoolean(property);
|
||||
}
|
||||
else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the property for the key, or the default value if the
|
||||
* property doesn't exist.
|
||||
* @param key The key.
|
||||
* @param defaultValue The default value.
|
||||
* @return The property or default value.
|
||||
*/
|
||||
public int getProperty(String key, int defaultValue) {
|
||||
String property = this.properties.getProperty(key);
|
||||
if (property != null) {
|
||||
return Integer.parseInt(property);
|
||||
}
|
||||
else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the property for the key, or the default value if the
|
||||
* property doesn't exist.
|
||||
* @param key The key.
|
||||
* @param defaultValue The default value.
|
||||
* @return The property or default value.
|
||||
*/
|
||||
public long getProperty(String key, long defaultValue) {
|
||||
String property = this.properties.getProperty(key);
|
||||
if (property != null) {
|
||||
return Long.parseLong(property);
|
||||
}
|
||||
else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the property for the key, or the default value if the
|
||||
* property doesn't exist.
|
||||
* @param key The key.
|
||||
* @param defaultValue The default value.
|
||||
* @return The property or default value.
|
||||
*/
|
||||
public double getProperty(String key, double defaultValue) {
|
||||
String property = properties.getProperty(key);
|
||||
if (property != null) {
|
||||
return Double.parseDouble(property);
|
||||
}
|
||||
else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the 'concurrency' property or the default value.
|
||||
* The meaning of concurrency depends on the bus implementation.
|
||||
* @param defaultValue The default value.
|
||||
* @return The property or default value.
|
||||
*/
|
||||
public int getConcurrency(int defaultValue) {
|
||||
return getProperty(CONCURRENCY, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the 'maxConcurrency' property or the default value.
|
||||
* The meaning of maxConcurrency depends on the bus implementation.
|
||||
* @param defaultValue The default value.
|
||||
* @return The property or default value.
|
||||
*/
|
||||
public int getMaxConcurrency(int defaultValue) {
|
||||
return getProperty(MAX_CONCURRENCY, defaultValue);
|
||||
}
|
||||
|
||||
// Retry properties
|
||||
|
||||
/**
|
||||
* Return the 'maxAttempts' property or the default value.
|
||||
* This is used in the retry template's SimpleRetryPolicy
|
||||
* in buses that support retry.
|
||||
* @param defaultValue The default value.
|
||||
* @return The property or default value.
|
||||
*/
|
||||
public int getMaxAttempts(int defaultValue) {
|
||||
return getProperty(MAX_ATTEMPTS, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the 'backOffInitialInterval' property or the default value.
|
||||
* This is used in the retry template's ExponentialBackOffPolicy
|
||||
* in buses that support retry.
|
||||
* @param defaultValue The default value.
|
||||
* @return The property or default value.
|
||||
*/
|
||||
public long getBackOffInitialInterval(long defaultValue) {
|
||||
return getProperty(BACK_OFF_INITIAL_INTERVAL, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the 'backOffMultiplier' property or the default value.
|
||||
* This is used in the retry template's ExponentialBackOffPolicy
|
||||
* in buses that support retry.
|
||||
* @param defaultValue The default value.
|
||||
* @return The property or default value.
|
||||
*/
|
||||
public double getBackOffMultiplier(double defaultValue) {
|
||||
return getProperty(BACK_OFF_MULTIPLIER, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the 'backOffMaxInterval' property or the default value.
|
||||
* This is used in the retry template's ExponentialBackOffPolicy
|
||||
* in buses that support retry.
|
||||
* @param defaultValue The default value.
|
||||
* @return The property or default value.
|
||||
*/
|
||||
public long getBackOffMaxInterval(long defaultValue) {
|
||||
return getProperty(BACK_OFF_MAX_INTERVAL, defaultValue);
|
||||
}
|
||||
|
||||
// Partitioning
|
||||
|
||||
/**
|
||||
* A class name for extracting partition keys from messages.
|
||||
* @return The class name,
|
||||
*/
|
||||
public String getPartitionKeyExtractorClass() {
|
||||
return getProperty(PARTITION_KEY_EXTRACTOR_CLASS);
|
||||
}
|
||||
|
||||
/**
|
||||
* The expression to determine the partition key, evaluated against the
|
||||
* message as the root object.
|
||||
* @return The key.
|
||||
*/
|
||||
public Expression getPartitionKeyExpression() {
|
||||
String partionKeyExpression = getProperty(PARTITION_KEY_EXPRESSION);
|
||||
Expression expression = null;
|
||||
if (partionKeyExpression != null) {
|
||||
expression = spelExpressionParser.parseExpression(partionKeyExpression);
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* A class name for calculating a partition from a key.
|
||||
* @return The class name,
|
||||
*/
|
||||
public String getPartitionSelectorClass() {
|
||||
return getProperty(PARTITION_SELECTOR_CLASS);
|
||||
}
|
||||
|
||||
/**
|
||||
* The expression evaluated against the partition key to determine
|
||||
* the partition to which the message will be sent. The result should
|
||||
* be an integer that will subsequently be mod'd with the module's
|
||||
* partition count.
|
||||
* @return The expression.
|
||||
*/
|
||||
public Expression getPartitionSelectorExpression() {
|
||||
String partionSelectorExpression = getProperty(PARTITION_SELECTOR_EXPRESSION);
|
||||
Expression expression = null;
|
||||
if (partionSelectorExpression != null) {
|
||||
expression = spelExpressionParser.parseExpression(partionSelectorExpression);
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* The sequence number for this module.
|
||||
*
|
||||
* @return the sequence number.
|
||||
*/
|
||||
public int getSequence() {
|
||||
return getProperty(SEQUENCE, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* The module count.
|
||||
*
|
||||
* @return the module count.
|
||||
*/
|
||||
public int getCount() {
|
||||
return getProperty(COUNT, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* The next module count for non-sink modules
|
||||
* @return the next module count
|
||||
*/
|
||||
public int getNextModuleCount() {
|
||||
return getProperty(NEXT_MODULE_COUNT, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* The partition index that this consumer supports.
|
||||
* @return The partition index.
|
||||
*/
|
||||
public int getPartitionIndex() {
|
||||
return getProperty(PARTITION_INDEX, -1);
|
||||
}
|
||||
|
||||
// Direct Binding
|
||||
|
||||
/**
|
||||
* If true, the bus can attempt a direct binding.
|
||||
*/
|
||||
public boolean isDirectBindingAllowed() {
|
||||
return getProperty(DIRECT_BINDING_ALLOWED, false);
|
||||
}
|
||||
|
||||
// Batching
|
||||
|
||||
/**
|
||||
* If true, enable batching.
|
||||
* @param defaultValue the default value.
|
||||
* @return the property or default value.
|
||||
*/
|
||||
public boolean isBatchingEnabled(boolean defaultValue) {
|
||||
return getProperty(BATCHING_ENABLED, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* The batch size.
|
||||
* @param defaultValue the default value.
|
||||
* @return the property or default value.
|
||||
*/
|
||||
public int getBatchSize(int defaultValue) {
|
||||
return getProperty(BATCH_SIZE, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* The batch buffer limit.
|
||||
* @param defaultValue the default value.
|
||||
* @return the property or default value.
|
||||
*/
|
||||
public int geteBatchBufferLimit(int defaultValue) {
|
||||
return getProperty(BATCH_BUFFER_LIMIT, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* The batch timeout.
|
||||
* @param defaultValue the default value.
|
||||
* @return the property or default value.
|
||||
*/
|
||||
public long getBatchTimeout(long defaultValue) {
|
||||
return getProperty(BATCH_TIMEOUT, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* If true, messages will be compressed.
|
||||
* @param defaultValue the default value.
|
||||
* @return the property or default value.
|
||||
*/
|
||||
public boolean isCompress(boolean defaultValue) {
|
||||
return getProperty(COMPRESS, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* If true, subscriptions to taps/topics will be durable.
|
||||
* @param defaultValue the default value.
|
||||
* @return the property or default value.
|
||||
*/
|
||||
public boolean isDurable(boolean defaultValue) {
|
||||
return getProperty(DURABLE, defaultValue);
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
|
||||
/**
|
||||
* Convert a comma-delimited String property to a String[] if
|
||||
* present, or return the default value.
|
||||
* @param value The property value.
|
||||
* @param defaultValue The default value.
|
||||
* @return The converted property or default value.
|
||||
*/
|
||||
protected String[] asStringArray(String value, String[] defaultValue) {
|
||||
if (StringUtils.hasText(value)) {
|
||||
return StringUtils.commaDelimitedListToStringArray(value);
|
||||
}
|
||||
else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.properties.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2013-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import 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 MessageBus. The binding
|
||||
* could be for a consumer or a producer. A consumer binding represents a connection from an adapter on the bus to a
|
||||
* module's input channel. A producer binding represents a connection from a module's output channel to an adapter on
|
||||
* the bus.
|
||||
*
|
||||
* @author Jennifer Hickey
|
||||
* @author Mark Fisher
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class Binding implements Lifecycle {
|
||||
|
||||
public static final String PRODUCER = "producer";
|
||||
|
||||
public static final String CONSUMER = "consumer";
|
||||
|
||||
public static final String DIRECT = "direct";
|
||||
|
||||
private final String name;
|
||||
|
||||
private final MessageChannel channel;
|
||||
|
||||
private final AbstractEndpoint endpoint;
|
||||
|
||||
private final String type;
|
||||
|
||||
private final AbstractBusPropertiesAccessor properties;
|
||||
|
||||
private Binding(String name, MessageChannel channel, AbstractEndpoint endpoint, String type,
|
||||
AbstractBusPropertiesAccessor properties) {
|
||||
Assert.notNull(channel, "channel must not be null");
|
||||
Assert.notNull(endpoint, "endpoint must not be null");
|
||||
this.name = name;
|
||||
this.channel = channel;
|
||||
this.endpoint = endpoint;
|
||||
this.type = type;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public static Binding forConsumer(String name, AbstractEndpoint adapterFromBus, MessageChannel moduleInputChannel,
|
||||
AbstractBusPropertiesAccessor properties) {
|
||||
return new Binding(name, moduleInputChannel, adapterFromBus, CONSUMER, properties);
|
||||
}
|
||||
|
||||
public static Binding forProducer(String name, MessageChannel moduleOutputChannel, AbstractEndpoint adapterToBus,
|
||||
AbstractBusPropertiesAccessor properties) {
|
||||
return new Binding(name, moduleOutputChannel, adapterToBus, PRODUCER, properties);
|
||||
}
|
||||
|
||||
public static Binding forDirectProducer(String name, MessageChannel moduleOutputChannel,
|
||||
AbstractEndpoint adapter, AbstractBusPropertiesAccessor properties) {
|
||||
return new Binding(name, moduleOutputChannel, adapter, DIRECT, properties);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public MessageChannel getChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
public AbstractEndpoint getEndpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public AbstractBusPropertiesAccessor getPropertiesAccessor() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
endpoint.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
endpoint.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return endpoint.isRunning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return type + " Binding [name=" + name + ", channel=" + channel + ", endpoint=" + endpoint.getComponentName()
|
||||
+ "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* Interface for implementations that perform cleanup for message buses.
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 1.2
|
||||
*/
|
||||
public interface BusCleaner {
|
||||
|
||||
/**
|
||||
* Clean up all resources for the supplied stream/job.
|
||||
* @param entity the stream or job; may be terminated with a simple wild card '*', in which
|
||||
* case all streams with names starting with the characters before the '*' will be cleaned.
|
||||
* @param isJob true if the entity is a job.
|
||||
* @return a map of lists of resources removed.
|
||||
*/
|
||||
Map<String, List<String>> clean(String entity, boolean isJob);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
|
||||
/**
|
||||
* Common bus properties.
|
||||
*
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public interface BusProperties {
|
||||
|
||||
/**
|
||||
* The retry back off initial interval.
|
||||
*/
|
||||
public static final String BACK_OFF_INITIAL_INTERVAL = "backOffInitialInterval";
|
||||
|
||||
/**
|
||||
* The retry back off max interval.
|
||||
*/
|
||||
public static final String BACK_OFF_MAX_INTERVAL = "backOffMaxInterval";
|
||||
|
||||
/**
|
||||
* The retry back off multiplier.
|
||||
*/
|
||||
public static final String BACK_OFF_MULTIPLIER = "backOffMultiplier";
|
||||
|
||||
/**
|
||||
* The minimum number of concurrent deliveries.
|
||||
*/
|
||||
public static final String CONCURRENCY = "concurrency";
|
||||
|
||||
/**
|
||||
* The maximum delivery attempts when a delivery fails.
|
||||
*/
|
||||
public static final String MAX_ATTEMPTS = "maxAttempts";
|
||||
|
||||
/**
|
||||
* The maximum number of concurrent deliveries.
|
||||
*/
|
||||
public static final String MAX_CONCURRENCY = "maxConcurrency";
|
||||
|
||||
/**
|
||||
* The sequence index of the module.
|
||||
* In a partitioned stream, it is identical to the partition index.
|
||||
*/
|
||||
public static final String SEQUENCE = "sequence";
|
||||
|
||||
/**
|
||||
* The number of consumers, i.e. module instances in the stream.
|
||||
* In a partitioned stream, it is identical to the partition count.
|
||||
*/
|
||||
public static final String COUNT = "count";
|
||||
|
||||
/**
|
||||
* The consumer's partition index.
|
||||
*/
|
||||
public static final String PARTITION_INDEX = "partitionIndex";
|
||||
|
||||
/**
|
||||
* The partition key expression.
|
||||
*/
|
||||
public static final String PARTITION_KEY_EXPRESSION = "partitionKeyExpression";
|
||||
|
||||
/**
|
||||
* The partition key class.
|
||||
*/
|
||||
public static final String PARTITION_KEY_EXTRACTOR_CLASS = "partitionKeyExtractorClass";
|
||||
|
||||
/**
|
||||
* The partition selector class.
|
||||
*/
|
||||
public static final String PARTITION_SELECTOR_CLASS = "partitionSelectorClass";
|
||||
|
||||
/**
|
||||
* The partition selector expression.
|
||||
*/
|
||||
public static final String PARTITION_SELECTOR_EXPRESSION = "partitionSelectorExpression";
|
||||
|
||||
/**
|
||||
* If true, the bus 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.
|
||||
*/
|
||||
public static final String BATCHING_ENABLED = "batchingEnabled";
|
||||
|
||||
/**
|
||||
* The batch size if batching is enabled.
|
||||
*/
|
||||
public static final String BATCH_SIZE = "batchSize";
|
||||
|
||||
/**
|
||||
* The buffer limit if batching is enabled.
|
||||
*/
|
||||
public static final String BATCH_BUFFER_LIMIT = "batchBufferLimit";
|
||||
|
||||
/**
|
||||
* The batch timeout if batching is enabled.
|
||||
*/
|
||||
public static final String BATCH_TIMEOUT = "batchTimeout";
|
||||
|
||||
/**
|
||||
* For all non-terminal modules, the number of modules coming after this one, irrespective of partitioning.
|
||||
*/
|
||||
public static final String NEXT_MODULE_COUNT = "next.module.count";
|
||||
|
||||
/**
|
||||
* For all non-terminal modules, the concurrency for module coming after this one.
|
||||
*/
|
||||
public static final String NEXT_MODULE_CONCURRENCY = "next.module.concurrency";
|
||||
|
||||
/**
|
||||
* Compression enabled.
|
||||
*/
|
||||
public static final String COMPRESS = "compress";
|
||||
|
||||
/**
|
||||
* Durable pub/sub consumer.
|
||||
*/
|
||||
public static final String DURABLE = "durableSubscription";
|
||||
|
||||
/**
|
||||
* Minimum partition count, if the transport supports partitioning natively (e.g. Kafka)
|
||||
*/
|
||||
public static final String MIN_PARTITION_COUNT = "minPartitionCount";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Message Bus utilities.
|
||||
*
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class BusUtils {
|
||||
|
||||
/**
|
||||
* The delimiter between a group and index when constructing a bus consumer/producer.
|
||||
*/
|
||||
public static final String GROUP_INDEX_DELIMITER = ".";
|
||||
|
||||
/**
|
||||
* The prefix for the consumer/producer when creating a tap.
|
||||
*/
|
||||
public static final String TAP_CHANNEL_PREFIX = "tap:";
|
||||
|
||||
/**
|
||||
* The prefix for the consumer/producer when creating a topic.
|
||||
*/
|
||||
public static final String TOPIC_CHANNEL_PREFIX = "topic:";
|
||||
|
||||
public static final Pattern PUBSUB_NAMED_CHANNEL_PATTERN = Pattern.compile("[^.]+\\.(tap|topic):");
|
||||
|
||||
public static String addGroupToPubSub(String group, String inputChannelName) {
|
||||
if (inputChannelName.startsWith(TAP_CHANNEL_PREFIX)
|
||||
|| inputChannelName.startsWith(TOPIC_CHANNEL_PREFIX)) {
|
||||
inputChannelName = group + "." + inputChannelName;
|
||||
}
|
||||
return inputChannelName;
|
||||
}
|
||||
|
||||
public static String removeGroupFromPubSub(String name) {
|
||||
if (PUBSUB_NAMED_CHANNEL_PATTERN.matcher(name).find()) {
|
||||
return name.substring(name.indexOf(".") + 1);
|
||||
}
|
||||
else {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.");
|
||||
// Check if the channelName starts with tap: or topic:
|
||||
return (channelName.startsWith(TAP_CHANNEL_PREFIX) || channelName.startsWith(TOPIC_CHANNEL_PREFIX));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a pipe name from the group and index.
|
||||
* @param group the group.
|
||||
* @param index the index.
|
||||
* @return the name.
|
||||
*/
|
||||
public static String constructPipeName(String group, int index) {
|
||||
return group + GROUP_INDEX_DELIMITER + index;
|
||||
}
|
||||
|
||||
public static String constructTapPrefix(String group) {
|
||||
return TAP_CHANNEL_PREFIX + "stream:" + group;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2014-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.xml.bind.DatatypeConverter;
|
||||
|
||||
import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.integration.support.json.Jackson2JsonObjectMapper;
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
* Encodes requested headers into payload with format
|
||||
* {@code 0xff, n(1), [ [lenHdr(1), hdr, lenValue(4), value] ... ]}.
|
||||
* The 0xff indicates this new format; n is number of headers (max 255); for
|
||||
* each header, the name length (1 byte) is followed by the name, followed by
|
||||
* the value length (int) followed by the value (json).
|
||||
* <p>
|
||||
* Previously, there was no leading 0xff; the value length was 1 byte and only
|
||||
* String header values were supported (no JSON conversion).
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class EmbeddedHeadersMessageConverter {
|
||||
|
||||
private final Jackson2JsonObjectMapper objectMapper = new Jackson2JsonObjectMapper();
|
||||
|
||||
public static String decodeExceptionMessage(Message<?> requestMessage) {
|
||||
return "Could not convert message: " + DatatypeConverter.printHexBinary((byte[]) requestMessage.getPayload());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a new message where some of the original headers of {@code original}
|
||||
* have been embedded into the new message payload.
|
||||
*/
|
||||
public byte[] embedHeaders(MessageValues original, String... headers) throws Exception {
|
||||
byte[][] headerValues = new byte[headers.length][];
|
||||
int n = 0;
|
||||
int headerCount = 0;
|
||||
int headersLength = 0;
|
||||
for (String header : headers) {
|
||||
Object value = original.get(header) == null ? null
|
||||
: original.get(header);
|
||||
if (value != null) {
|
||||
String json = this.objectMapper.toJson(value);
|
||||
headerValues[n++] = json.getBytes("UTF-8");
|
||||
headerCount++;
|
||||
headersLength += header.length() + json.length();
|
||||
}
|
||||
else {
|
||||
headerValues[n++] = null;
|
||||
}
|
||||
}
|
||||
// 0xff, n(1), [ [lenHdr(1), hdr, lenValue(4), value] ... ]
|
||||
byte[] newPayload = new byte[((byte[])original.getPayload()).length + headersLength + headerCount * 5 + 2];
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(newPayload);
|
||||
byteBuffer.put((byte) 0xff); // signal new format
|
||||
byteBuffer.put((byte) headerCount);
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
if (headerValues[i] != null) {
|
||||
byteBuffer.put((byte) headers[i].length());
|
||||
byteBuffer.put(headers[i].getBytes("UTF-8"));
|
||||
byteBuffer.putInt(headerValues[i].length);
|
||||
byteBuffer.put(headerValues[i]);
|
||||
}
|
||||
}
|
||||
|
||||
byteBuffer.put((byte[])original.getPayload());
|
||||
return byteBuffer.array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a message where headers, that were originally embedded into the payload, have been promoted
|
||||
* back to actual headers. The new payload is now the original payload.
|
||||
*
|
||||
* @param message the message to extract headers
|
||||
* @param copyRequestHeaders boolean value to specify if original headers should be copied
|
||||
*/
|
||||
public MessageValues extractHeaders(Message<byte[]> message, boolean copyRequestHeaders) throws Exception {
|
||||
byte[] bytes = message.getPayload();
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
|
||||
int headerCount = byteBuffer.get() & 0xff;
|
||||
if (headerCount < 255) {
|
||||
return oldExtractHeaders(byteBuffer, bytes, headerCount, message, copyRequestHeaders);
|
||||
}
|
||||
else {
|
||||
headerCount = byteBuffer.get() & 0xff;
|
||||
Map<String, Object> headers = new HashMap<String, Object>();
|
||||
for (int i = 0; i < headerCount; i++) {
|
||||
int len = byteBuffer.get() & 0xff;
|
||||
String headerName = new String(bytes, byteBuffer.position(), len, "UTF-8");
|
||||
byteBuffer.position(byteBuffer.position() + len);
|
||||
len = byteBuffer.getInt();
|
||||
String headerValue = new String(bytes, byteBuffer.position(), len, "UTF-8");
|
||||
Object headerContent = this.objectMapper.fromJson(headerValue, Object.class);
|
||||
headers.put(headerName, headerContent);
|
||||
byteBuffer.position(byteBuffer.position() + len);
|
||||
}
|
||||
byte[] newPayload = new byte[byteBuffer.remaining()];
|
||||
byteBuffer.get(newPayload);
|
||||
return buildMessageValues(message, newPayload, headers, copyRequestHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
private MessageValues oldExtractHeaders(ByteBuffer byteBuffer, byte[] bytes, int headerCount,
|
||||
Message<byte[]> message, boolean copyRequestHeaders)
|
||||
throws UnsupportedEncodingException {
|
||||
Map<String, Object> headers = new HashMap<String, Object>();
|
||||
for (int i = 0; i < headerCount; i++) {
|
||||
int len = byteBuffer.get();
|
||||
String headerName = new String(bytes, byteBuffer.position(), len, "UTF-8");
|
||||
byteBuffer.position(byteBuffer.position() + len);
|
||||
len = byteBuffer.get() & 0xff;
|
||||
String headerValue = new String(bytes, byteBuffer.position(), len, "UTF-8");
|
||||
byteBuffer.position(byteBuffer.position() + len);
|
||||
if (IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER.equals(headerName)
|
||||
|| IntegrationMessageHeaderAccessor.SEQUENCE_SIZE.equals(headerName)) {
|
||||
headers.put(headerName, Integer.parseInt(headerValue));
|
||||
}
|
||||
else {
|
||||
headers.put(headerName, headerValue);
|
||||
}
|
||||
}
|
||||
byte[] newPayload = new byte[byteBuffer.remaining()];
|
||||
byteBuffer.get(newPayload);
|
||||
return buildMessageValues(message, newPayload, headers, copyRequestHeaders);
|
||||
}
|
||||
|
||||
private MessageValues buildMessageValues(Message<byte[]> message, byte[] payload, Map<String, Object> headers,
|
||||
boolean copyRequestHeaders) {
|
||||
MessageValues messageValues = new MessageValues(payload, headers);
|
||||
if (copyRequestHeaders) {
|
||||
messageValues.copyHeadersIfAbsent(message.getHeaders());
|
||||
}
|
||||
return messageValues;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2013-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
|
||||
/**
|
||||
* A strategy interface used to bind a {@link MessageChannel} to a logical name. The name is intended to identify a
|
||||
* logical consumer or producer of messages. This may be a queue, a channel adapter, another message channel, a Spring
|
||||
* bean, etc.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author David Turanski
|
||||
* @author Gary Russell
|
||||
* @author Jennifer Hickey
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @since 1.0
|
||||
*/
|
||||
public interface MessageBus {
|
||||
|
||||
/**
|
||||
* Bind a message consumer on a p2p channel
|
||||
*
|
||||
* @param name the logical identity of the message source
|
||||
* @param moduleInputChannel the channel bound as a consumer
|
||||
* @param properties arbitrary String key/value pairs that will be used in the binding
|
||||
*/
|
||||
void bindConsumer(String name, MessageChannel moduleInputChannel, Properties properties);
|
||||
|
||||
|
||||
/**
|
||||
* Bind a message consumer on a pub/sub channel
|
||||
*
|
||||
* @param name the logical identity of the message source
|
||||
* @param inputChannel the channel bound as a pub/sub consumer
|
||||
* @param properties arbitrary String key/value pairs that will be used in the binding
|
||||
*/
|
||||
void bindPubSubConsumer(final String name, MessageChannel inputChannel, Properties properties);
|
||||
|
||||
/**
|
||||
* Bind a message producer on a p2p channel.
|
||||
*
|
||||
* @param name the logical identity of the message target
|
||||
* @param moduleOutputChannel the channel bound as a producer
|
||||
* @param properties arbitrary String key/value pairs that will be used in the binding
|
||||
*/
|
||||
void bindProducer(String name, MessageChannel moduleOutputChannel, Properties properties);
|
||||
|
||||
|
||||
/**
|
||||
* Bind a message producer on a pub/sub channel.
|
||||
*
|
||||
* @param name the logical identity of the message target
|
||||
* @param outputChannel the channel bound as a producer
|
||||
* @param properties arbitrary String key/value pairs that will be used in the binding
|
||||
*/
|
||||
void bindPubSubProducer(final String name, MessageChannel outputChannel, Properties properties);
|
||||
|
||||
/**
|
||||
* Unbind an inbound inter-module channel and stop any active components that use the channel.
|
||||
*
|
||||
* @param name the channel name
|
||||
*/
|
||||
void unbindConsumers(String name);
|
||||
|
||||
/**
|
||||
* Unbind an outbound inter-module channel 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 channel The channel bound as a consumer
|
||||
*/
|
||||
void unbindConsumer(String name, MessageChannel channel);
|
||||
|
||||
/**
|
||||
* Unbind a specific p2p or pub/sub message producer
|
||||
*
|
||||
* @param name the logical identity of the message target
|
||||
* @param channel the channel bound as a producer
|
||||
*/
|
||||
void unbindProducer(String name, MessageChannel channel);
|
||||
|
||||
/**
|
||||
* Bind a producer that expects async replies. To unbind, invoke unbindProducer() and unbindConsumer().
|
||||
*
|
||||
* @param name The name of the requestor.
|
||||
* @param requests The request channel - sends requests.
|
||||
* @param replies The reply channel - receives replies.
|
||||
* @param properties arbitrary String key/value pairs that will be used in the binding.
|
||||
*/
|
||||
void bindRequestor(String name, MessageChannel requests, MessageChannel 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 request channel - receives requests.
|
||||
* @param replies The reply channel - sends replies.
|
||||
* @param properties arbitrary String key/value pairs that will be used in the binding.
|
||||
*/
|
||||
void bindReplier(String name, MessageChannel requests, MessageChannel replies, Properties properties);
|
||||
|
||||
/**
|
||||
* Create a channel and bind a producer dynamically, creating the infrastructure
|
||||
* required by the bus 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 channel.
|
||||
*/
|
||||
MessageChannel bindDynamicProducer(String name, Properties properties);
|
||||
|
||||
/**
|
||||
* Create a channel and bind a producer dynamically, creating the infrastructure
|
||||
* required by the bus 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 channel.
|
||||
*/
|
||||
MessageChannel bindDynamicPubSubProducer(String name, Properties properties);
|
||||
|
||||
/**
|
||||
* Return true if the bus supports the capability.
|
||||
* @param capability the capability.
|
||||
* @return true if the capability is supported.
|
||||
*/
|
||||
boolean isCapable(Capability capability);
|
||||
|
||||
public enum Capability {
|
||||
|
||||
/**
|
||||
* When a bus supports durable subscriptions to a pub/sub channel, the stream
|
||||
* name will be included in the consumer name.
|
||||
*/
|
||||
DURABLE_PUBSUB
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
|
||||
/**
|
||||
* Exception thrown to indicate a message bus error (most
|
||||
* likely a configuration error).
|
||||
*
|
||||
* @author Gary Russell
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class MessageBusException extends RuntimeException {
|
||||
|
||||
public MessageBusException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.integration.support.MessageBuilderFactory;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A mutable type for allowing {@link MessageBus} implementations to transform and enrich message content more
|
||||
* efficiently.
|
||||
* @author David Turanski
|
||||
*/
|
||||
public class MessageValues implements Map<String, Object> {
|
||||
private Map<String, Object> headers = new HashMap<>();
|
||||
|
||||
private Object payload;
|
||||
|
||||
/**
|
||||
* Create an instance from a {@link Message}.
|
||||
* @param message the message
|
||||
*/
|
||||
public MessageValues(Message<?> message) {
|
||||
this.payload = message.getPayload();
|
||||
for (Map.Entry<String, Object> header : message.getHeaders().entrySet()) {
|
||||
this.headers.put(header.getKey(), header.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
public MessageValues(Object payload, Map<String,Object> headers) {
|
||||
this.payload = payload;
|
||||
this.headers.putAll(headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the payload
|
||||
*/
|
||||
public Object getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to a {@link Message} using a {@link org.springframework.integration.support.MessageBuilderFactory}.
|
||||
* @param messageBuilderFactory the MessageBuilderFactory
|
||||
* @return the Message
|
||||
*/
|
||||
public Message<?> toMessage(MessageBuilderFactory messageBuilderFactory) {
|
||||
return messageBuilderFactory.withPayload(this.payload).copyHeaders(this.headers).build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert to a {@link Message} using a the default {@link org.springframework.integration.support.MessageBuilder}.
|
||||
* @return the Message
|
||||
*/
|
||||
public Message<?> toMessage() {
|
||||
return MessageBuilder.withPayload(this.payload).copyHeaders(this.headers).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the payload
|
||||
* @param payload any non null object.
|
||||
*/
|
||||
public void setPayload(Object payload) {
|
||||
Assert.notNull(payload, "'payload' cannot be null");
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return headers.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return headers.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
return headers.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsValue(Object value) {
|
||||
return headers.containsValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get(Object key) {
|
||||
return headers.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object put(String key, Object value) {
|
||||
return headers.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object remove(Object key) {
|
||||
return headers.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(Map<? extends String, ?> m) {
|
||||
headers.putAll(m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
headers.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> keySet() {
|
||||
return headers.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Object> values() {
|
||||
return headers.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<String, Object>> entrySet() {
|
||||
return headers.entrySet();
|
||||
}
|
||||
|
||||
public void copyHeadersIfAbsent(Map<String,Object> headersToCopy) {
|
||||
for (Entry<String, Object> headersToCopyEntry : headersToCopy.entrySet()) {
|
||||
if (!containsKey(headersToCopyEntry.getKey())) {
|
||||
put(headersToCopyEntry.getKey(), headersToCopyEntry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
|
||||
/**
|
||||
* Strategy for extracting a partition key from a Message.
|
||||
*
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public interface PartitionKeyExtractorStrategy {
|
||||
|
||||
Object extractKey(Message<?> message);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
|
||||
/**
|
||||
* Strategy for determining the partition to which a message should be sent.
|
||||
*
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public interface PartitionSelectorStrategy {
|
||||
|
||||
/**
|
||||
* Determine the partition based on a key. The partitionCount is 1 greater
|
||||
* than the maximum value of a valid partition. Typical implementations
|
||||
* will return {@code someValue % partitionCount}. The caller will apply
|
||||
* that same modulo operation (as well as enforcing absolute value) if the
|
||||
* value exceeds partitionCount - 1.
|
||||
*
|
||||
* @param key the key
|
||||
* @param partitionCount the number of partitions
|
||||
*
|
||||
* @return the partition
|
||||
*/
|
||||
int selectPartition(Object key, int partitionCount);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
|
||||
import org.springframework.cloud.streams.exception.CloudStreamsRuntimeException;
|
||||
|
||||
/**
|
||||
* Exceptions thrown while interfacing with the RabbitMQ admin plugin.
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 1.2
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class RabbitAdminException extends CloudStreamsRuntimeException {
|
||||
|
||||
public RabbitAdminException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public RabbitAdminException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.auth.AuthScope;
|
||||
import org.apache.http.auth.UsernamePasswordCredentials;
|
||||
import org.apache.http.client.AuthCache;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.protocol.HttpClientContext;
|
||||
import org.apache.http.impl.auth.BasicScheme;
|
||||
import org.apache.http.impl.client.BasicAuthCache;
|
||||
import org.apache.http.impl.client.BasicCredentialsProvider;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @since 1.2
|
||||
*/
|
||||
public class RabbitManagementUtils {
|
||||
|
||||
public static RestTemplate buildRestTemplate(String adminUri, String user, String password) {
|
||||
BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
|
||||
credsProvider.setCredentials(
|
||||
new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT),
|
||||
new UsernamePasswordCredentials(user, password));
|
||||
HttpClient httpClient = HttpClients.custom().setDefaultCredentialsProvider(credsProvider).build();
|
||||
// Set up pre-emptive basic Auth because the rabbit plugin doesn't currently support challenge/response for PUT
|
||||
// Create AuthCache instance
|
||||
AuthCache authCache = new BasicAuthCache();
|
||||
// Generate BASIC scheme object and add it to the local; from the apache docs...
|
||||
// auth cache
|
||||
BasicScheme basicAuth = new BasicScheme();
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(adminUri);
|
||||
}
|
||||
catch (URISyntaxException e) {
|
||||
throw new RabbitAdminException("Invalid URI", e);
|
||||
}
|
||||
authCache.put(new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme()), basicAuth);
|
||||
// Add AuthCache to the execution context
|
||||
final HttpClientContext localContext = HttpClientContext.create();
|
||||
localContext.setAuthCache(authCache);
|
||||
RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient) {
|
||||
|
||||
@Override
|
||||
protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
|
||||
return localContext;
|
||||
}
|
||||
|
||||
});
|
||||
restTemplate.setMessageConverters(Collections.<HttpMessageConverter<?>>singletonList(
|
||||
new MappingJackson2HttpMessageConverter()));
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.converter.DefaultContentTypeResolver;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
/**
|
||||
* A {@link DefaultContentTypeResolver} that can parse String values.
|
||||
*
|
||||
* @author David Turanski
|
||||
*/
|
||||
public class StringConvertingContentTypeResolver extends DefaultContentTypeResolver {
|
||||
|
||||
private ConcurrentMap<String,MimeType> mimeTypeCache = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public MimeType resolve(MessageHeaders headers) {
|
||||
return resolve((Map<String, Object>) headers);
|
||||
}
|
||||
|
||||
public MimeType resolve(Map<String,Object> headers) {
|
||||
Object value = headers.get(MessageHeaders.CONTENT_TYPE);
|
||||
if (value instanceof MimeType) {
|
||||
return (MimeType) value;
|
||||
}
|
||||
else if (value instanceof String) {
|
||||
MimeType mimeType = mimeTypeCache.get(value);
|
||||
if (mimeType == null) {
|
||||
String valueAsString = (String) value;
|
||||
mimeType = MimeType.valueOf(valueAsString);
|
||||
mimeTypeCache.put(valueAsString,mimeType);
|
||||
}
|
||||
return mimeType;
|
||||
}
|
||||
return getDefaultMimeType();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
|
||||
|
||||
/**
|
||||
* Spring Integration message headers for XD.
|
||||
*
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public final class XdHeaders {
|
||||
|
||||
public static final String XD_REPLY_CHANNEL = "xdReplyChannel";
|
||||
|
||||
public static final String XD_HISTORY = "xdHistory";
|
||||
|
||||
/*
|
||||
* no xd prefix for backwards compatibility
|
||||
*/
|
||||
public static final String XD_ORIGINAL_CONTENT_TYPE = "originalContentType";
|
||||
|
||||
/*
|
||||
* no xd prefix for backwards compatibility
|
||||
*/
|
||||
public static final String REPLY_TO = "replyTo";
|
||||
|
||||
/**
|
||||
* The headers that will be propagated, by default, by message bus implementations
|
||||
* that have no inherent header support (by embedding the headers in the payload).
|
||||
*/
|
||||
public static final String[] STANDARD_HEADERS = new String[] {
|
||||
IntegrationMessageHeaderAccessor.CORRELATION_ID,
|
||||
IntegrationMessageHeaderAccessor.SEQUENCE_SIZE,
|
||||
IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER,
|
||||
XD_REPLY_CHANNEL,
|
||||
MessageHeaders.CONTENT_TYPE,
|
||||
XD_ORIGINAL_CONTENT_TYPE,
|
||||
REPLY_TO,
|
||||
XD_HISTORY
|
||||
};
|
||||
|
||||
private XdHeaders() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @since 1.0
|
||||
*
|
||||
*/
|
||||
public class MessageConverterTests {
|
||||
|
||||
@Test
|
||||
public void testHeaderEmbedding() throws Exception {
|
||||
EmbeddedHeadersMessageConverter converter = new EmbeddedHeadersMessageConverter();
|
||||
Message<byte[]> message = MessageBuilder.withPayload("Hello".getBytes())
|
||||
.setHeader("foo", "bar")
|
||||
.setHeader("baz", "quxx")
|
||||
.build();
|
||||
byte[] embedded = converter.embedHeaders(new MessageValues(message), "foo", "baz");
|
||||
assertEquals(0xff, embedded[0] & 0xff);
|
||||
assertEquals("\u0002\u0003foo\u0000\u0000\u0000\u0005\"bar\"\u0003baz\u0000\u0000\u0000\u0006\"quxx\"Hello",
|
||||
new String(embedded).substring(1));
|
||||
|
||||
MessageValues extracted = converter.extractHeaders(MessageBuilder.withPayload(embedded).build(), false);
|
||||
assertEquals("Hello", new String((byte[])extracted.getPayload()));
|
||||
assertEquals("bar", extracted.get("foo"));
|
||||
assertEquals("quxx", extracted.get("baz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHeaderEmbeddingMissingHeader() throws Exception {
|
||||
EmbeddedHeadersMessageConverter converter = new EmbeddedHeadersMessageConverter();
|
||||
Message<byte[]> message = MessageBuilder.withPayload("Hello".getBytes())
|
||||
.setHeader("foo", "bar")
|
||||
.build();
|
||||
byte[] embedded = converter.embedHeaders(new MessageValues(message), "foo", "baz");
|
||||
assertEquals(0xff, embedded[0] & 0xff);
|
||||
assertEquals("\u0001\u0003foo\u0000\u0000\u0000\u0005\"bar\"Hello",
|
||||
new String(embedded).substring(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanDecodeOldFormat() throws Exception {
|
||||
EmbeddedHeadersMessageConverter converter = new EmbeddedHeadersMessageConverter();
|
||||
byte[] bytes = "\u0002\u0003foo\u0003bar\u0003baz\u0004quxxHello".getBytes("UTF-8");
|
||||
Message<byte[]> message = new GenericMessage<byte[]>(bytes);
|
||||
MessageValues extracted = converter.extractHeaders(message,false);
|
||||
assertEquals("Hello", new String((byte[])extracted.getPayload()));
|
||||
assertEquals("bar", extracted.get("foo"));
|
||||
assertEquals("quxx", extracted.get("baz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadDecode() throws Exception {
|
||||
EmbeddedHeadersMessageConverter converter = new EmbeddedHeadersMessageConverter();
|
||||
byte[] bytes = "\u0002\u0003foo\u0020bar\u0003baz\u0004quxxHello".getBytes("UTF-8");
|
||||
Message<byte[]> message = new GenericMessage<byte[]>(bytes);
|
||||
try {
|
||||
converter.extractHeaders(message,false);
|
||||
Assert.fail("Exception expected");
|
||||
}
|
||||
catch (Exception e) {
|
||||
String s = EmbeddedHeadersMessageConverter.decodeExceptionMessage(message);
|
||||
assertThat(e, instanceOf(StringIndexOutOfBoundsException.class));
|
||||
assertThat(s, startsWith("Could not convert message: 0203666F6F"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
Spring XD Test Support
|
||||
======================
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-test</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>spring-cloud-streams-binding-test</name>
|
||||
<description>Test support for binding implementations</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-bindings-parent</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<spring-amqp.version>1.4.5.RELEASE</spring-amqp.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<!-- There's no comparison with gradle's syntax, but I think we'll be happy sticking with
|
||||
maven for the superior dependency and project management -->
|
||||
|
||||
<!--
|
||||
compile ("org.springframework.integration:spring-integration-test"){
|
||||
exclude group: 'org.apache.avro', module: 'avro-compiler'
|
||||
|
||||
}
|
||||
compile "org.springframework.integration:spring-integration-amqp"
|
||||
compile "org.springframework.integration:spring-integration-redis"
|
||||
compile "org.springframework.integration:spring-integration-mqtt"
|
||||
compile ("org.springframework.integration:spring-integration-kafka:$springIntegrationKafkaVersion") {
|
||||
exclude group: 'org.apache.avro', module: 'avro-compiler'
|
||||
}
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.integration</groupId>
|
||||
<artifactId>spring-integration-test</artifactId>
|
||||
<version>${spring-integration.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.avro</groupId>
|
||||
<artifactId>avro-compiler</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!--<dependency>-->
|
||||
<!--<groupId>org.springframework.integration</groupId>-->
|
||||
<!--<artifactId>spring-integration-redis</artifactId>-->
|
||||
<!--<version>${spring-integration.version}</version>-->
|
||||
<!--</dependency>-->
|
||||
<!--<dependency>-->
|
||||
<!--<groupId>org.springframework.integration</groupId>-->
|
||||
<!--<artifactId>spring-integration-mqtt</artifactId>-->
|
||||
<!--<version>${spring-integration.version}</version>-->
|
||||
<!--</dependency>-->
|
||||
<!--<dependency>-->
|
||||
<!--<groupId>org.springframework.integration</groupId>-->
|
||||
<!--<artifactId>spring-integration-kafka</artifactId>-->
|
||||
<!--<version>1.1.2.RELEASE</version>-->
|
||||
<!--<exclusions>-->
|
||||
<!--<exclusion>-->
|
||||
<!--<groupId>org.apache.avro</groupId>-->
|
||||
<!--<artifactId>avro-compiler</artifactId>-->
|
||||
<!--</exclusion>-->
|
||||
<!--</exclusions>-->
|
||||
<!--</dependency>-->
|
||||
<!--<dependency>-->
|
||||
<!--<groupId>org.apache.kafka</groupId>-->
|
||||
<!--<artifactId>kafka_2.10</artifactId>-->
|
||||
<!--</dependency>-->
|
||||
<!--<dependency>-->
|
||||
<!--<groupId>org.apache.kafka</groupId>-->
|
||||
<!--<artifactId>kafka_2.10</artifactId>-->
|
||||
<!--<classifier>test</classifier>-->
|
||||
<!--</dependency>-->
|
||||
<!--<dependency>-->
|
||||
<!--<groupId>org.apache.kafka</groupId>-->
|
||||
<!--<artifactId>kafka-clients</artifactId>-->
|
||||
<!--</dependency>-->
|
||||
<!--<dependency>-->
|
||||
<!--<groupId>org.apache.curator</groupId>-->
|
||||
<!--<artifactId>curator-test</artifactId>-->
|
||||
<!--</dependency>-->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
<version>${spring-framework.version}</version>
|
||||
</dependency>
|
||||
<!--<dependency>-->
|
||||
<!--<groupId>org.springframework.amqp</groupId>-->
|
||||
<!--<artifactId>spring-rabbit</artifactId>-->
|
||||
<!--<version>${spring-amqp.version}</version>-->
|
||||
<!--</dependency>-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-tuple</artifactId>
|
||||
<version>${spring-xd.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-codec</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-spi</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-codec</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
* Copyright 2013-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import 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;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.channel.QueueChannel;
|
||||
import org.springframework.integration.channel.interceptor.WireTap;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.xd.dirt.integration.bus.MessageBus.Capability;
|
||||
import org.springframework.xd.dirt.integration.bus.serializer.MultiTypeCodec;
|
||||
import org.springframework.xd.dirt.integration.bus.serializer.kryo.PojoCodec;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @author David Turanski
|
||||
*/
|
||||
public abstract class AbstractMessageBusTests {
|
||||
|
||||
protected static final Collection<MediaType> ALL = Collections.singletonList(MediaType.ALL);
|
||||
|
||||
protected AbstractTestMessageBus<?> testMessageBus;
|
||||
|
||||
@Test
|
||||
public void testClean() throws Exception {
|
||||
MessageBus messageBus = getMessageBus();
|
||||
messageBus.bindProducer("foo.0", new DirectChannel(), null);
|
||||
messageBus.bindConsumer("foo.0", new DirectChannel(), null);
|
||||
messageBus.bindProducer("foo.1", new DirectChannel(), null);
|
||||
messageBus.bindConsumer("foo.1", new DirectChannel(), null);
|
||||
messageBus.bindProducer("foo.2", new DirectChannel(), null);
|
||||
Collection<?> bindings = getBindings(messageBus);
|
||||
assertEquals(5, bindings.size());
|
||||
messageBus.unbindProducers("foo.0");
|
||||
assertEquals(4, bindings.size());
|
||||
messageBus.unbindConsumers("foo.0");
|
||||
messageBus.unbindProducers("foo.1");
|
||||
assertEquals(2, bindings.size());
|
||||
messageBus.unbindConsumers("foo.1");
|
||||
messageBus.unbindProducers("foo.2");
|
||||
assertTrue(bindings.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendAndReceive() throws Exception {
|
||||
MessageBus messageBus = getMessageBus();
|
||||
DirectChannel moduleOutputChannel = new DirectChannel();
|
||||
QueueChannel moduleInputChannel = new QueueChannel();
|
||||
messageBus.bindProducer("foo.0", moduleOutputChannel, null);
|
||||
messageBus.bindConsumer("foo.0", moduleInputChannel, null);
|
||||
Message<?> message = MessageBuilder.withPayload("foo").setHeader(MessageHeaders.CONTENT_TYPE,
|
||||
"foo/bar").build();
|
||||
// Let the consumer actually bind to the producer before sending a msg
|
||||
busBindUnbindLatency();
|
||||
moduleOutputChannel.send(message);
|
||||
Message<?> inbound = moduleInputChannel.receive(5000);
|
||||
assertNotNull(inbound);
|
||||
assertEquals("foo", inbound.getPayload());
|
||||
assertNull(inbound.getHeaders().get(XdHeaders.XD_ORIGINAL_CONTENT_TYPE));
|
||||
assertEquals("foo/bar", inbound.getHeaders().get(MessageHeaders.CONTENT_TYPE));
|
||||
messageBus.unbindProducers("foo.0");
|
||||
messageBus.unbindConsumers("foo.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendAndReceiveNoOriginalContentType() throws Exception {
|
||||
MessageBus messageBus = getMessageBus();
|
||||
DirectChannel moduleOutputChannel = new DirectChannel();
|
||||
QueueChannel moduleInputChannel = new QueueChannel();
|
||||
messageBus.bindProducer("bar.0", moduleOutputChannel, null);
|
||||
messageBus.bindConsumer("bar.0", moduleInputChannel, null);
|
||||
busBindUnbindLatency();
|
||||
|
||||
Message<?> message = MessageBuilder.withPayload("foo").build();
|
||||
moduleOutputChannel.send(message);
|
||||
Message<?> inbound = moduleInputChannel.receive(5000);
|
||||
assertNotNull(inbound);
|
||||
assertEquals("foo", inbound.getPayload());
|
||||
assertNull(inbound.getHeaders().get(XdHeaders.XD_ORIGINAL_CONTENT_TYPE));
|
||||
assertNull(inbound.getHeaders().get(MessageHeaders.CONTENT_TYPE));
|
||||
messageBus.unbindProducers("bar.0");
|
||||
messageBus.unbindConsumers("bar.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendAndReceivePubSub() throws Exception {
|
||||
MessageBus messageBus = getMessageBus();
|
||||
DirectChannel moduleOutputChannel = new DirectChannel();
|
||||
// Test pub/sub by emulating how StreamPlugin handles taps
|
||||
DirectChannel tapChannel = new DirectChannel();
|
||||
QueueChannel moduleInputChannel = new QueueChannel();
|
||||
QueueChannel module2InputChannel = new QueueChannel();
|
||||
QueueChannel module3InputChannel = new QueueChannel();
|
||||
messageBus.bindProducer("baz.0", moduleOutputChannel, null);
|
||||
messageBus.bindConsumer("baz.0", moduleInputChannel, null);
|
||||
moduleOutputChannel.addInterceptor(new WireTap(tapChannel));
|
||||
messageBus.bindPubSubProducer("tap:baz.http", tapChannel, null);
|
||||
// A new module is using the tap as an input channel
|
||||
String fooTapName = messageBus.isCapable(Capability.DURABLE_PUBSUB) ? "foo.tap:baz.http" : "tap:baz.http";
|
||||
messageBus.bindPubSubConsumer(fooTapName, module2InputChannel, null);
|
||||
// Another new module is using tap as an input channel
|
||||
String barTapName = messageBus.isCapable(Capability.DURABLE_PUBSUB) ? "bar.tap:baz.http" : "tap:baz.http";
|
||||
messageBus.bindPubSubConsumer(barTapName, module3InputChannel, 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(XdHeaders.XD_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(XdHeaders.XD_ORIGINAL_CONTENT_TYPE));
|
||||
assertEquals("foo/bar", tapped1.getHeaders().get(MessageHeaders.CONTENT_TYPE));
|
||||
assertEquals("foo", tapped2.getPayload());
|
||||
assertNull(tapped2.getHeaders().get(XdHeaders.XD_ORIGINAL_CONTENT_TYPE));
|
||||
assertEquals("foo/bar", tapped2.getHeaders().get(MessageHeaders.CONTENT_TYPE));
|
||||
}
|
||||
// delete one tap stream is deleted
|
||||
messageBus.unbindConsumer(barTapName, module3InputChannel);
|
||||
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
|
||||
messageBus.unbindConsumer(fooTapName, module2InputChannel);
|
||||
// Clean up as StreamPlugin would
|
||||
messageBus.unbindConsumer("baz.0", moduleInputChannel);
|
||||
messageBus.unbindProducer("baz.0", moduleOutputChannel);
|
||||
messageBus.unbindProducers("tap:baz.http");
|
||||
assertTrue(getBindings(messageBus).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createInboundPubSubBeforeOutboundPubSub() throws Exception {
|
||||
MessageBus messageBus = getMessageBus();
|
||||
DirectChannel moduleOutputChannel = new DirectChannel();
|
||||
// Test pub/sub by emulating how StreamPlugin handles taps
|
||||
DirectChannel tapChannel = new DirectChannel();
|
||||
QueueChannel moduleInputChannel = new QueueChannel();
|
||||
QueueChannel module2InputChannel = new QueueChannel();
|
||||
QueueChannel module3InputChannel = new QueueChannel();
|
||||
// Create the tap first
|
||||
String fooTapName = messageBus.isCapable(Capability.DURABLE_PUBSUB) ? "foo.tap:baz.http" : "tap:baz.http";
|
||||
messageBus.bindPubSubConsumer(fooTapName, module2InputChannel, null);
|
||||
|
||||
// Then create the stream
|
||||
messageBus.bindProducer("baz.0", moduleOutputChannel, null);
|
||||
messageBus.bindConsumer("baz.0", moduleInputChannel, null);
|
||||
moduleOutputChannel.addInterceptor(new WireTap(tapChannel));
|
||||
messageBus.bindPubSubProducer("tap:baz.http", tapChannel, null);
|
||||
|
||||
// Another new module is using tap as an input channel
|
||||
String barTapName = messageBus.isCapable(Capability.DURABLE_PUBSUB) ? "bar.tap:baz.http" : "tap:baz.http";
|
||||
messageBus.bindPubSubConsumer(barTapName, module3InputChannel, 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(XdHeaders.XD_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(XdHeaders.XD_ORIGINAL_CONTENT_TYPE));
|
||||
assertEquals("foo/bar", tapped1.getHeaders().get(MessageHeaders.CONTENT_TYPE));
|
||||
assertEquals("foo", tapped2.getPayload());
|
||||
assertNull(tapped2.getHeaders().get(XdHeaders.XD_ORIGINAL_CONTENT_TYPE));
|
||||
assertEquals("foo/bar", tapped2.getHeaders().get(MessageHeaders.CONTENT_TYPE));
|
||||
}
|
||||
// delete one tap stream is deleted
|
||||
messageBus.unbindConsumer(barTapName, module3InputChannel);
|
||||
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
|
||||
messageBus.unbindConsumer(fooTapName, module2InputChannel);
|
||||
// Clean up as StreamPlugin would
|
||||
messageBus.unbindConsumer("baz.0", moduleInputChannel);
|
||||
messageBus.unbindProducer("baz.0", moduleOutputChannel);
|
||||
messageBus.unbindProducers("tap:baz.http");
|
||||
assertTrue(getBindings(messageBus).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadDynamic() throws Exception {
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty(BusProperties.PARTITION_KEY_EXPRESSION, "'foo'");
|
||||
MessageBus messageBus = getMessageBus();
|
||||
try {
|
||||
messageBus.bindDynamicProducer("queue:foo", properties);
|
||||
fail("Exception expected");
|
||||
}
|
||||
catch (MessageBusException mbe) {
|
||||
Assert.assertEquals("Failed to bind dynamic channel 'queue:foo' with properties " +
|
||||
"{partitionKeyExpression='foo'}",
|
||||
mbe.getMessage());
|
||||
if (messageBus instanceof AbstractTestMessageBus) {
|
||||
messageBus = ((AbstractTestMessageBus) messageBus).getCoreMessageBus();
|
||||
}
|
||||
assertFalse(((MessageBusSupport) messageBus).getApplicationContext().containsBean("queue:foo"));
|
||||
}
|
||||
}
|
||||
|
||||
protected Collection<?> getBindings(MessageBus testMessageBus) {
|
||||
if (testMessageBus instanceof AbstractTestMessageBus) {
|
||||
return getBindingsFromMsgBus(((AbstractTestMessageBus) testMessageBus).getCoreMessageBus());
|
||||
}
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
protected Collection<?> getBindingsFromMsgBus(MessageBus messageBus) {
|
||||
DirectFieldAccessor accessor = new DirectFieldAccessor(messageBus);
|
||||
return (List<?>) accessor.getPropertyValue("bindings");
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
protected MultiTypeCodec<Object> getCodec() {
|
||||
return new PojoCodec();
|
||||
}
|
||||
|
||||
protected abstract MessageBus getMessageBus() throws Exception;
|
||||
|
||||
@After
|
||||
public void cleanup() {
|
||||
if (testMessageBus != null) {
|
||||
testMessageBus.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If appropriate, let the bus middleware settle down a bit while binding/unbinding actually happens.
|
||||
*/
|
||||
protected void busBindUnbindLatency() throws InterruptedException {
|
||||
// default none
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2014-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
|
||||
|
||||
/**
|
||||
* Abstract class that adds test support for {@link MessageBus}.
|
||||
*
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public abstract class AbstractTestMessageBus<C extends MessageBusSupport> implements MessageBus {
|
||||
|
||||
protected Set<String> queues = new HashSet<String>();
|
||||
|
||||
protected Set<String> topics = new HashSet<String>();
|
||||
|
||||
private C messageBus;
|
||||
|
||||
public void setMessageBus(C messageBus) {
|
||||
messageBus.setIntegrationEvaluationContext(new StandardEvaluationContext());
|
||||
try {
|
||||
messageBus.afterPropertiesSet();
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException("Failed to initialize message bus", e);
|
||||
}
|
||||
this.messageBus = messageBus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindConsumer(String name, MessageChannel moduleInputChannel, Properties properties) {
|
||||
messageBus.bindConsumer(name, moduleInputChannel, properties);
|
||||
queues.add(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindPubSubConsumer(String name, MessageChannel inputChannel, Properties properties) {
|
||||
messageBus.bindPubSubConsumer(name, inputChannel, properties);
|
||||
addTopic(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindProducer(String name, MessageChannel moduleOutputChannel, Properties properties) {
|
||||
messageBus.bindProducer(name, moduleOutputChannel, properties);
|
||||
queues.add(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindPubSubProducer(String name, MessageChannel outputChannel, Properties properties) {
|
||||
messageBus.bindPubSubProducer(name, outputChannel, properties);
|
||||
addTopic(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindRequestor(String name, MessageChannel requests, MessageChannel replies,
|
||||
Properties properties) {
|
||||
messageBus.bindRequestor(name, requests, replies, properties);
|
||||
queues.add(name + ".requests");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindReplier(String name, MessageChannel requests, MessageChannel replies,
|
||||
Properties properties) {
|
||||
messageBus.bindReplier(name, requests, replies, properties);
|
||||
queues.add(name + ".requests");
|
||||
}
|
||||
|
||||
private void addTopic(String topicName) {
|
||||
topics.add("topic." + topicName);
|
||||
}
|
||||
|
||||
public C getCoreMessageBus() {
|
||||
return messageBus;
|
||||
}
|
||||
|
||||
public abstract void cleanup();
|
||||
|
||||
@Override
|
||||
public void unbindConsumers(String name) {
|
||||
messageBus.unbindConsumers(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbindProducers(String name) {
|
||||
messageBus.unbindProducers(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbindConsumer(String name, MessageChannel channel) {
|
||||
messageBus.unbindConsumer(name, channel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbindProducer(String name, MessageChannel channel) {
|
||||
messageBus.unbindProducer(name, channel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageChannel bindDynamicProducer(String name, Properties properties) {
|
||||
this.queues.add(name);
|
||||
return this.messageBus.bindDynamicProducer(name, properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageChannel bindDynamicPubSubProducer(String name, Properties properties) {
|
||||
this.topics.add(name);
|
||||
return this.messageBus.bindDynamicPubSubProducer(name, properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCapable(Capability capability) {
|
||||
return this.messageBus.isCapable(capability);
|
||||
}
|
||||
|
||||
public MessageBus getMessageBus() {
|
||||
return this.messageBus;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import static org.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 buses that use an external broker.
|
||||
*
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public abstract class BrokerBusTests extends
|
||||
AbstractMessageBusTests {
|
||||
|
||||
@Test
|
||||
public void testDirectBinding() throws Exception {
|
||||
MessageBus bus = getMessageBus();
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty(BusProperties.DIRECT_BINDING_ALLOWED, "true");
|
||||
|
||||
DirectChannel moduleInputChannel = new DirectChannel();
|
||||
moduleInputChannel.setBeanName("direct.input");
|
||||
DirectChannel moduleOutputChannel = new DirectChannel();
|
||||
moduleOutputChannel.setBeanName("direct.output");
|
||||
bus.bindConsumer("direct.0", moduleInputChannel, null);
|
||||
bus.bindProducer("direct.0", moduleOutputChannel, properties);
|
||||
|
||||
final AtomicReference<Thread> caller = new AtomicReference<Thread>();
|
||||
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<String>("foo"));
|
||||
moduleOutputChannel.send(new GenericMessage<String>("foo"));
|
||||
|
||||
assertNotNull(caller.get());
|
||||
assertSame(Thread.currentThread(), caller.get());
|
||||
assertEquals(2, count.get());
|
||||
assertNull(spyOn("direct.0").receive(true));
|
||||
|
||||
// Remove direct binding and bind producer to the bus
|
||||
bus.unbindConsumers("direct.0");
|
||||
busBindUnbindLatency();
|
||||
|
||||
Spy spy = spyOn("direct.0");
|
||||
count.set(0);
|
||||
moduleOutputChannel.send(new GenericMessage<String>("bar"));
|
||||
moduleOutputChannel.send(new GenericMessage<String>("baz"));
|
||||
Object bar = spy.receive(false);
|
||||
assertEquals("bar", bar);
|
||||
Object baz = spy.receive(false);
|
||||
assertEquals("baz", baz);
|
||||
assertEquals(0, count.get());
|
||||
|
||||
// Unbind producer from bus and bind directly again
|
||||
caller.set(null);
|
||||
bus.bindConsumer("direct.0", moduleInputChannel, null);
|
||||
moduleOutputChannel.send(new GenericMessage<String>("foo"));
|
||||
moduleOutputChannel.send(new GenericMessage<String>("foo"));
|
||||
assertNotNull(caller.get());
|
||||
assertSame(Thread.currentThread(), caller.get());
|
||||
assertEquals(2, count.get());
|
||||
assertNull(spy.receive(true));
|
||||
|
||||
bus.unbindProducers("direct.0");
|
||||
bus.unbindConsumers("direct.0");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new spy on the given 'queue'. This allows de-correlating the creation of
|
||||
* the 'connection' from its actual usage, which may be needed by some implementations to
|
||||
* see messages sent after connection creation.
|
||||
*/
|
||||
public abstract Spy spyOn(final String name);
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.support.AbstractApplicationContext;
|
||||
import org.springframework.integration.support.DefaultMessageBuilderFactory;
|
||||
import org.springframework.integration.support.MessageBuilderFactory;
|
||||
import org.springframework.integration.support.utils.IntegrationUtils;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class BusTestUtils {
|
||||
|
||||
private static final MessageBuilderFactory mbf = new DefaultMessageBuilderFactory();
|
||||
|
||||
public static final AbstractApplicationContext MOCK_AC = mock(AbstractApplicationContext.class);
|
||||
|
||||
public static final ConfigurableListableBeanFactory MOCK_BF = mock(ConfigurableListableBeanFactory.class);
|
||||
|
||||
static {
|
||||
when(MOCK_BF.getBean(IntegrationUtils.INTEGRATION_MESSAGE_BUILDER_FACTORY_BEAN_NAME,
|
||||
MessageBuilderFactory.class)).thenReturn(mbf);
|
||||
when(MOCK_AC.getBean(IntegrationUtils.INTEGRATION_MESSAGE_BUILDER_FACTORY_BEAN_NAME,
|
||||
MessageBuilderFactory.class)).thenReturn(mbf);
|
||||
when(MOCK_AC.getBeanFactory()).thenReturn(MOCK_BF);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasProperty;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.hamcrest.CustomMatcher;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.channel.QueueChannel;
|
||||
import org.springframework.integration.endpoint.AbstractEndpoint;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.xd.test.TestUtils;
|
||||
|
||||
|
||||
/**
|
||||
* Tests for buses that support partitioning.
|
||||
*
|
||||
* @author Gary Russell
|
||||
*/
|
||||
abstract public class PartitionCapableBusTests extends BrokerBusTests {
|
||||
|
||||
@Test
|
||||
public void testBadProperties() throws Exception {
|
||||
MessageBus bus = getMessageBus();
|
||||
Properties properties = new Properties();
|
||||
properties.put("foo", "bar");
|
||||
properties.put("baz", "qux");
|
||||
|
||||
DirectChannel output = new DirectChannel();
|
||||
try {
|
||||
bus.bindProducer("badprops.0", output, properties);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertThat(e.getMessage(), allOf(Matchers.containsString(bus.getClass().getSimpleName().replace("Test", "")
|
||||
+ " does not support producer "),
|
||||
containsString("foo"),
|
||||
containsString("baz"),
|
||||
containsString(" for badprops.0.")));
|
||||
}
|
||||
|
||||
properties.remove("baz");
|
||||
try {
|
||||
bus.bindConsumer("badprops.0", output, properties);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
assertThat(e.getMessage(), equalTo(bus.getClass().getSimpleName().replace("Test", "")
|
||||
+ " does not support consumer property: foo for badprops.0."));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartitionedModuleSpEL() throws Exception {
|
||||
MessageBus bus = getMessageBus();
|
||||
Properties properties = new Properties();
|
||||
properties.put("partitionKeyExpression", "payload");
|
||||
properties.put("partitionSelectorExpression", "hashCode()");
|
||||
properties.put(BusProperties.NEXT_MODULE_COUNT, "3");
|
||||
properties.put(BusProperties.NEXT_MODULE_CONCURRENCY, "2");
|
||||
|
||||
DirectChannel output = new DirectChannel();
|
||||
output.setBeanName("test.output");
|
||||
bus.bindProducer("part.0", output, properties);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.bindings", List.class);
|
||||
assertEquals(1, bindings.size());
|
||||
try {
|
||||
AbstractEndpoint endpoint = bindings.get(0).getEndpoint();
|
||||
assertThat(getEndpointRouting(endpoint), containsString("part.0-' + headers['partition']"));
|
||||
}
|
||||
catch (UnsupportedOperationException ignored) {
|
||||
|
||||
}
|
||||
|
||||
properties.clear();
|
||||
properties.put("concurrency", "2");
|
||||
properties.put("partitionIndex", "0");
|
||||
properties.put("count","3");
|
||||
QueueChannel input0 = new QueueChannel();
|
||||
input0.setBeanName("test.input0S");
|
||||
bus.bindConsumer("part.0", input0, properties);
|
||||
properties.put("partitionIndex", "1");
|
||||
QueueChannel input1 = new QueueChannel();
|
||||
input1.setBeanName("test.input1S");
|
||||
bus.bindConsumer("part.0", input1, properties);
|
||||
properties.put("partitionIndex", "2");
|
||||
QueueChannel input2 = new QueueChannel();
|
||||
input2.setBeanName("test.input2S");
|
||||
bus.bindConsumer("part.0", input2, properties);
|
||||
|
||||
Message<Integer> message2 = MessageBuilder.withPayload(2)
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CORRELATION_ID, "foo")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER, 42)
|
||||
.setHeader(IntegrationMessageHeaderAccessor.SEQUENCE_SIZE, 43)
|
||||
.setHeader("xdReplyChannel", "bar")
|
||||
.build();
|
||||
output.send(message2);
|
||||
output.send(new GenericMessage<Integer>(1));
|
||||
output.send(new GenericMessage<Integer>(0));
|
||||
|
||||
Message<?> receive0 = input0.receive(1000);
|
||||
assertNotNull(receive0);
|
||||
Message<?> receive1 = input1.receive(1000);
|
||||
assertNotNull(receive1);
|
||||
Message<?> receive2 = input2.receive(1000);
|
||||
assertNotNull(receive2);
|
||||
|
||||
Matcher<Message<?>> fooMatcher = new CustomMatcher<Message<?>>("the message with 'foo' as its correlationId") {
|
||||
|
||||
@Override
|
||||
public boolean matches(Object item) {
|
||||
IntegrationMessageHeaderAccessor accessor = new IntegrationMessageHeaderAccessor((Message<?>) item);
|
||||
boolean result = "foo".equals(accessor.getCorrelationId()) &&
|
||||
42 == accessor.getSequenceNumber() &&
|
||||
43 == accessor.getSequenceSize() &&
|
||||
"bar".equals(accessor.getHeader("xdReplyChannel"));
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
if (usesExplicitRouting()) {
|
||||
assertEquals(0, receive0.getPayload());
|
||||
assertEquals(1, receive1.getPayload());
|
||||
assertEquals(2, receive2.getPayload());
|
||||
|
||||
assertThat(receive2, fooMatcher);
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
assertThat(Arrays.asList(
|
||||
(Integer) receive0.getPayload(),
|
||||
(Integer) receive1.getPayload(),
|
||||
(Integer) receive2.getPayload()),
|
||||
containsInAnyOrder(0, 1, 2));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Matcher<Iterable<? extends Message<?>>> containsOur3Messages = containsInAnyOrder(
|
||||
fooMatcher,
|
||||
hasProperty("payload", equalTo(0)),
|
||||
hasProperty("payload", equalTo(1))
|
||||
);
|
||||
assertThat(
|
||||
Arrays.asList(receive0, receive1, receive2),
|
||||
containsOur3Messages);
|
||||
|
||||
}
|
||||
|
||||
bus.unbindConsumers("part.0");
|
||||
bus.unbindProducers("part.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartitionedModuleJava() throws Exception {
|
||||
MessageBus bus = getMessageBus();
|
||||
Properties properties = new Properties();
|
||||
properties.put("partitionKeyExtractorClass", "org.springframework.xd.dirt.integration.bus.PartitionTestSupport");
|
||||
properties.put("partitionSelectorClass", "org.springframework.xd.dirt.integration.bus.PartitionTestSupport");
|
||||
properties.put(BusProperties.NEXT_MODULE_COUNT, "3");
|
||||
properties.put(BusProperties.NEXT_MODULE_CONCURRENCY, "2");
|
||||
|
||||
DirectChannel output = new DirectChannel();
|
||||
output.setBeanName("test.output");
|
||||
bus.bindProducer("partJ.0", output, properties);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Binding> bindings = TestUtils.getPropertyValue(bus, "messageBus.bindings", List.class);
|
||||
assertEquals(1, bindings.size());
|
||||
if (usesExplicitRouting()) {
|
||||
AbstractEndpoint endpoint = bindings.get(0).getEndpoint();
|
||||
assertThat(getEndpointRouting(endpoint), containsString("partJ.0-' + headers['partition']"));
|
||||
}
|
||||
|
||||
properties.clear();
|
||||
properties.put("concurrency", "2");
|
||||
properties.put("count","3");
|
||||
properties.put("partitionIndex", "0");
|
||||
QueueChannel input0 = new QueueChannel();
|
||||
input0.setBeanName("test.input0J");
|
||||
bus.bindConsumer("partJ.0", input0, properties);
|
||||
properties.put("partitionIndex", "1");
|
||||
QueueChannel input1 = new QueueChannel();
|
||||
input1.setBeanName("test.input1J");
|
||||
bus.bindConsumer("partJ.0", input1, properties);
|
||||
properties.put("partitionIndex", "2");
|
||||
QueueChannel input2 = new QueueChannel();
|
||||
input2.setBeanName("test.input2J");
|
||||
bus.bindConsumer("partJ.0", input2, properties);
|
||||
|
||||
output.send(new GenericMessage<Integer>(2));
|
||||
output.send(new GenericMessage<Integer>(1));
|
||||
output.send(new GenericMessage<Integer>(0));
|
||||
|
||||
Message<?> receive0 = input0.receive(1000);
|
||||
assertNotNull(receive0);
|
||||
Message<?> receive1 = input1.receive(1000);
|
||||
assertNotNull(receive1);
|
||||
Message<?> receive2 = input2.receive(1000);
|
||||
assertNotNull(receive2);
|
||||
|
||||
if (usesExplicitRouting()) {
|
||||
assertEquals(0, receive0.getPayload());
|
||||
assertEquals(1, receive1.getPayload());
|
||||
assertEquals(2, receive2.getPayload());
|
||||
}
|
||||
else {
|
||||
|
||||
assertThat(Arrays.asList(
|
||||
(Integer) receive0.getPayload(),
|
||||
(Integer) receive1.getPayload(),
|
||||
(Integer) receive2.getPayload()),
|
||||
containsInAnyOrder(0, 1, 2));
|
||||
}
|
||||
|
||||
bus.unbindConsumers("partJ.0");
|
||||
bus.unbindProducers("partJ.0");
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementations should return whether the bus under test uses "explicit" routing (e.g. Rabbit)
|
||||
* whereby XD is responsible for assigning a partition and knows which exact consumer will receive the
|
||||
* message (i.e. honor "partitionIndex") or "implicit" routing (e.g. Kafka) whereby the only guarantee
|
||||
* is that messages will be spread, but we don't control exactly which consumer gets which message.
|
||||
*/
|
||||
protected abstract boolean usesExplicitRouting();
|
||||
|
||||
/**
|
||||
* For implementations that rely on explicit routing, return the routing expression.
|
||||
*/
|
||||
protected String getEndpointRouting(AbstractEndpoint endpoint) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* For implementations that rely on explicit routing, return the routing expression.
|
||||
*/
|
||||
protected String getPubSubEndpointRouting(AbstractEndpoint endpoint) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class PartitionTestSupport implements PartitionKeyExtractorStrategy, PartitionSelectorStrategy {
|
||||
|
||||
@Override
|
||||
public int selectPartition(Object key, int divisor) {
|
||||
return key.hashCode() % divisor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object extractKey(Message<?> message) {
|
||||
return message.getPayload();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
/**
|
||||
* Represents an out-of-band connection to the underlying middleware,
|
||||
* so that tests can check that some messages actually do (or do not)
|
||||
* transit through it.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public interface Spy {
|
||||
|
||||
public Object receive(boolean expectNull) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.test;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Rule;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
|
||||
/**
|
||||
* Abstract base class for JUnit {@link Rule}s that detect the presence of some external resource. If the resource is
|
||||
* indeed present, it will be available during the test lifecycle through {@link #getResource()}. If it is not, tests
|
||||
* will either fail or be skipped, depending on the value of system property {@value #XD_EXTERNAL_SERVERS_REQUIRED}.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public abstract class AbstractExternalResourceTestSupport<R> implements TestRule {
|
||||
|
||||
public static final String XD_EXTERNAL_SERVERS_REQUIRED = "XD_EXTERNAL_SERVERS_REQUIRED";
|
||||
|
||||
protected R resource;
|
||||
|
||||
private String resourceDescription;
|
||||
|
||||
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
protected AbstractExternalResourceTestSupport(String resourceDescription) {
|
||||
Assert.hasText(resourceDescription, "resourceDescription is required");
|
||||
this.resourceDescription = resourceDescription;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statement apply(final Statement base, Description description) {
|
||||
try {
|
||||
obtainResource();
|
||||
}
|
||||
catch (Exception e) {
|
||||
maybeCleanup();
|
||||
|
||||
return failOrSkip(e);
|
||||
}
|
||||
|
||||
return new Statement() {
|
||||
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
try {
|
||||
base.evaluate();
|
||||
}
|
||||
finally {
|
||||
try {
|
||||
cleanupResource();
|
||||
}
|
||||
catch (Exception ignored) {
|
||||
logger.warn("Exception while trying to cleanup proper resource", ignored);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
private Statement failOrSkip(final Exception e) {
|
||||
String serversRequired = System.getenv(XD_EXTERNAL_SERVERS_REQUIRED);
|
||||
if ("true".equalsIgnoreCase(serversRequired)) {
|
||||
logger.error(resourceDescription + " IS REQUIRED BUT NOT AVAILABLE", e);
|
||||
fail(resourceDescription + " IS NOT AVAILABLE");
|
||||
// Never reached, here to satisfy method signature
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
logger.error(resourceDescription + " IS NOT AVAILABLE, SKIPPING TESTS", e);
|
||||
return new Statement() {
|
||||
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
Assume.assumeTrue("Skipping test due to " + resourceDescription + " not being available " + e, false);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeCleanup() {
|
||||
if (resource != null) {
|
||||
try {
|
||||
cleanupResource();
|
||||
}
|
||||
catch (Exception ignored) {
|
||||
logger.warn("Exception while trying to cleanup failed resource", ignored);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public R getResource() {
|
||||
return resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform cleanup of the {@link #resource} field, which is guaranteed to be non null.
|
||||
*
|
||||
* @throws Exception any exception thrown by this method will be logged and swallowed
|
||||
*/
|
||||
protected abstract void cleanupResource() throws Exception;
|
||||
|
||||
/**
|
||||
* Try to obtain and validate a resource. Implementors should either set the {@link #resource} field with a valid
|
||||
* resource and return normally, or throw an exception.
|
||||
*/
|
||||
protected abstract void obtainResource() throws Exception;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.xd.test;
|
||||
|
||||
import org.springframework.beans.DirectFieldAccessor;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Copy of class in org.springframework.amqp.utils.test to avoid dependency on spring-amqp
|
||||
*/
|
||||
public class TestUtils {
|
||||
|
||||
/**
|
||||
* Uses nested {@link DirectFieldAccessor}s to obtain a property using dotted notation to traverse fields; e.g.
|
||||
* "foo.bar.baz" will obtain a reference to the baz field of the bar field of foo. Adopted from Spring Integration.
|
||||
* @param root The object.
|
||||
* @param propertyPath The path.
|
||||
* @return The field.
|
||||
*/
|
||||
public static Object getPropertyValue(Object root, String propertyPath) {
|
||||
Object value = null;
|
||||
DirectFieldAccessor accessor = new DirectFieldAccessor(root);
|
||||
String[] tokens = propertyPath.split("\\.");
|
||||
for (int i = 0; i < tokens.length; i++) {
|
||||
value = accessor.getPropertyValue(tokens[i]);
|
||||
if (value != null) {
|
||||
accessor = new DirectFieldAccessor(value);
|
||||
}
|
||||
else if (i == tokens.length - 1) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("intermediate property '" + tokens[i] + "' is null");
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T getPropertyValue(Object root, String propertyPath, Class<T> type) {
|
||||
Object value = getPropertyValue(root, propertyPath);
|
||||
if (value != null) {
|
||||
Assert.isAssignable(type, value.getClass());
|
||||
}
|
||||
return (T) value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
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.converter.ContentTypeResolver;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.xd.dirt.integration.bus.MessageBusSupport.JavaClassMimeTypeConversion;
|
||||
import org.springframework.xd.dirt.integration.bus.serializer.kryo.PojoCodec;
|
||||
import org.springframework.xd.tuple.DefaultTuple;
|
||||
import org.springframework.xd.tuple.Tuple;
|
||||
import org.springframework.xd.tuple.TupleBuilder;
|
||||
import org.springframework.xd.tuple.serializer.kryo.TupleKryoRegistrar;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @author David Turanski
|
||||
*/
|
||||
public class MessageBusSupportTests {
|
||||
|
||||
private ContentTypeResolver contentTypeResolver = new StringConvertingContentTypeResolver();
|
||||
|
||||
private final TestMessageBus messageBus = new TestMessageBus();
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
@Before
|
||||
public void setUp() {
|
||||
messageBus.setCodec(new PojoCodec(new TupleKryoRegistrar()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBytesPassThru() {
|
||||
byte[] payload = "foo".getBytes();
|
||||
Message<byte[]> message = MessageBuilder.withPayload(payload).build();
|
||||
MessageValues converted = messageBus.serializePayloadIfNecessary(message
|
||||
);
|
||||
assertSame(payload, converted.getPayload());
|
||||
Message<?> convertedMessage = converted.toMessage();
|
||||
assertSame(payload, convertedMessage.getPayload());
|
||||
assertEquals(MimeTypeUtils.APPLICATION_OCTET_STREAM,
|
||||
contentTypeResolver.resolve(convertedMessage.getHeaders()));
|
||||
MessageValues reconstructed = messageBus.deserializePayloadIfNecessary(convertedMessage);
|
||||
payload = (byte[]) reconstructed.getPayload();
|
||||
assertSame(converted.getPayload(), payload);
|
||||
assertNull(reconstructed.get(XdHeaders.XD_ORIGINAL_CONTENT_TYPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBytesPassThruContentType() {
|
||||
byte[] payload = "foo".getBytes();
|
||||
Message<byte[]> message = MessageBuilder.withPayload(payload)
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE)
|
||||
.build();
|
||||
MessageValues messageValues = messageBus.serializePayloadIfNecessary(message
|
||||
);
|
||||
Message<?> converted = messageValues.toMessage();
|
||||
assertSame(payload, converted.getPayload());
|
||||
assertEquals(MimeTypeUtils.APPLICATION_OCTET_STREAM,
|
||||
contentTypeResolver.resolve(converted.getHeaders()));
|
||||
MessageValues reconstructed = messageBus.deserializePayloadIfNecessary(converted);
|
||||
payload = (byte[]) reconstructed.getPayload();
|
||||
assertSame(converted.getPayload(), payload);
|
||||
assertEquals(MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE,
|
||||
reconstructed.get(MessageHeaders.CONTENT_TYPE));
|
||||
assertNull(reconstructed.get(XdHeaders.XD_ORIGINAL_CONTENT_TYPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testString() throws IOException {
|
||||
MessageValues convertedValues = messageBus.serializePayloadIfNecessary(
|
||||
new GenericMessage<String>("foo"));
|
||||
|
||||
Message<?> converted = convertedValues.toMessage();
|
||||
assertEquals(MimeTypeUtils.TEXT_PLAIN,
|
||||
contentTypeResolver.resolve(converted.getHeaders()));
|
||||
MessageValues reconstructed = messageBus.deserializePayloadIfNecessary(converted);
|
||||
assertEquals("foo", reconstructed.getPayload());
|
||||
assertNull(reconstructed.get(MessageHeaders.CONTENT_TYPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContentTypePreserved() throws IOException {
|
||||
Message<String> inbound = MessageBuilder.withPayload("{\"foo\":\"foo\"}")
|
||||
.copyHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON))
|
||||
.build();
|
||||
MessageValues convertedValues = messageBus.serializePayloadIfNecessary(
|
||||
inbound);
|
||||
|
||||
Message<?> converted = convertedValues.toMessage();
|
||||
|
||||
assertEquals(MimeTypeUtils.TEXT_PLAIN,
|
||||
contentTypeResolver.resolve(converted.getHeaders()));
|
||||
assertEquals(MimeTypeUtils.APPLICATION_JSON,
|
||||
converted.getHeaders().get(XdHeaders.XD_ORIGINAL_CONTENT_TYPE));
|
||||
MessageValues reconstructed = messageBus.deserializePayloadIfNecessary(converted);
|
||||
assertEquals("{\"foo\":\"foo\"}", reconstructed.getPayload());
|
||||
assertEquals(MimeTypeUtils.APPLICATION_JSON, reconstructed.get(MessageHeaders.CONTENT_TYPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPojoSerialization() {
|
||||
MessageValues convertedValues = messageBus.serializePayloadIfNecessary(
|
||||
new GenericMessage<Foo>(new Foo("bar"))
|
||||
);
|
||||
Message<?> converted = convertedValues.toMessage();
|
||||
MimeType mimeType = contentTypeResolver.resolve(converted.getHeaders());
|
||||
assertEquals("application", mimeType.getType());
|
||||
assertEquals("x-java-object", mimeType.getSubtype());
|
||||
assertEquals(Foo.class.getName(), mimeType.getParameter("type"));
|
||||
|
||||
MessageValues reconstructed = messageBus.deserializePayloadIfNecessary(converted);
|
||||
assertEquals("bar", ((Foo) reconstructed.getPayload()).getBar());
|
||||
assertNull(reconstructed.get(MessageHeaders.CONTENT_TYPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPojoWithXJavaObjectMimeTypeNoType() {
|
||||
MessageValues convertedValues = messageBus.serializePayloadIfNecessary(
|
||||
new GenericMessage<Foo>(new Foo("bar"))
|
||||
);
|
||||
Message<?> converted = convertedValues.toMessage();
|
||||
MimeType mimeType = contentTypeResolver.resolve(converted.getHeaders());
|
||||
assertEquals("application", mimeType.getType());
|
||||
assertEquals("x-java-object", mimeType.getSubtype());
|
||||
assertEquals(Foo.class.getName(), mimeType.getParameter("type"));
|
||||
|
||||
MessageValues reconstructed = messageBus.deserializePayloadIfNecessary(converted);
|
||||
assertEquals("bar", ((Foo) reconstructed.getPayload()).getBar());
|
||||
assertNull(reconstructed.get(MessageHeaders.CONTENT_TYPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPojoWithXJavaObjectMimeTypeExplicitType() {
|
||||
MessageValues convertedValues = messageBus.serializePayloadIfNecessary(
|
||||
new GenericMessage<Foo>(new Foo("bar"))
|
||||
);
|
||||
Message<?> converted = convertedValues.toMessage();
|
||||
MimeType mimeType = contentTypeResolver.resolve(converted.getHeaders());
|
||||
assertEquals("application", mimeType.getType());
|
||||
assertEquals("x-java-object", mimeType.getSubtype());
|
||||
assertEquals(Foo.class.getName(), mimeType.getParameter("type"));
|
||||
|
||||
MessageValues reconstructed = messageBus.deserializePayloadIfNecessary(converted);
|
||||
assertEquals("bar", ((Foo) reconstructed.getPayload()).getBar());
|
||||
assertNull(reconstructed.get(MessageHeaders.CONTENT_TYPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTupleSerialization() {
|
||||
Tuple payload = TupleBuilder.tuple().of("foo", "bar");
|
||||
MessageValues convertedValues = messageBus.serializePayloadIfNecessary(new GenericMessage<Tuple>(payload)
|
||||
);
|
||||
Message<?> converted = convertedValues.toMessage();
|
||||
MimeType mimeType = contentTypeResolver.resolve(converted.getHeaders());
|
||||
assertEquals("application", mimeType.getType());
|
||||
assertEquals("x-java-object", mimeType.getSubtype());
|
||||
assertEquals(DefaultTuple.class.getName(), mimeType.getParameter("type"));
|
||||
|
||||
MessageValues reconstructed = messageBus.deserializePayloadIfNecessary(converted);
|
||||
assertEquals("bar", ((Tuple) reconstructed.getPayload()).getString("foo"));
|
||||
assertNull(reconstructed.get(MessageHeaders.CONTENT_TYPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mimeTypeIsSimpleObject() throws ClassNotFoundException {
|
||||
MimeType mt = JavaClassMimeTypeConversion.mimeTypeFromObject(new Object());
|
||||
String className = JavaClassMimeTypeConversion.classNameFromMimeType(mt);
|
||||
assertEquals(Object.class, Class.forName(className));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mimeTypeIsObjectArray() throws ClassNotFoundException {
|
||||
MimeType mt = JavaClassMimeTypeConversion.mimeTypeFromObject(new String[0]);
|
||||
String className = JavaClassMimeTypeConversion.classNameFromMimeType(mt);
|
||||
assertEquals(String[].class, Class.forName(className));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mimeTypeIsMultiDimensionalObjectArray() throws ClassNotFoundException {
|
||||
MimeType mt = JavaClassMimeTypeConversion.mimeTypeFromObject(new String[0][0][0]);
|
||||
String className = JavaClassMimeTypeConversion.classNameFromMimeType(mt);
|
||||
assertEquals(String[][][].class, Class.forName(className));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mimeTypeIsPrimitiveArray() throws ClassNotFoundException {
|
||||
MimeType mt = JavaClassMimeTypeConversion.mimeTypeFromObject(new int[0]);
|
||||
String className = JavaClassMimeTypeConversion.classNameFromMimeType(mt);
|
||||
assertEquals(int[].class, Class.forName(className));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mimeTypeIsMultiDimensionalPrimitiveArray() throws ClassNotFoundException {
|
||||
MimeType mt = JavaClassMimeTypeConversion.mimeTypeFromObject(new int[0][0][0]);
|
||||
String className = JavaClassMimeTypeConversion.classNameFromMimeType(mt);
|
||||
assertEquals(int[][][].class, Class.forName(className));
|
||||
}
|
||||
|
||||
public static class Foo {
|
||||
|
||||
private String bar;
|
||||
|
||||
public Foo() {
|
||||
}
|
||||
|
||||
public Foo(String bar) {
|
||||
this.bar = bar;
|
||||
}
|
||||
|
||||
public String getBar() {
|
||||
return bar;
|
||||
}
|
||||
|
||||
public void setBar(String bar) {
|
||||
this.bar = bar;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Bar {
|
||||
|
||||
private String foo;
|
||||
|
||||
public Bar() {
|
||||
}
|
||||
|
||||
public Bar(String foo) {
|
||||
this.foo = foo;
|
||||
}
|
||||
|
||||
public String getFoo() {
|
||||
return foo;
|
||||
}
|
||||
|
||||
public void setFoo(String foo) {
|
||||
this.foo = foo;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class TestMessageBus extends MessageBusSupport {
|
||||
|
||||
@Override
|
||||
public void bindConsumer(String name, MessageChannel channel, Properties properties) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindPubSubConsumer(String name, MessageChannel moduleInputChannel,
|
||||
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) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,11 +3,8 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-codec</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>spring-cloud-streams-codec</name>
|
||||
<description>Serialization library used by transport</description>
|
||||
|
||||
@@ -19,7 +16,6 @@
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-common</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>spring-cloud-streams-common</name>
|
||||
<description>Spring Cloud Streams common components</description>
|
||||
|
||||
@@ -19,7 +16,6 @@
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-sample-double</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>spring-cloud-streams-sample-double</name>
|
||||
@@ -29,8 +27,8 @@
|
||||
<artifactId>spring-cloud-streams</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-messagebus-redis</artifactId>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-sample-extended</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>spring-cloud-streams-sample-extended</name>
|
||||
<description>Demo project for Spring XD module</description>
|
||||
|
||||
@@ -41,8 +38,8 @@
|
||||
<artifactId>spring-cloud-streams-sample-sink</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-messagebus-redis</artifactId>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
|
||||
<artifactId>spring-cloud-streams-samples</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<url>http://projects.spring.io/spring-xd/</url>
|
||||
<organization>
|
||||
@@ -28,17 +27,17 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-sample-source</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-sample-sink</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-sample-transform</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-sample-sink</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>spring-cloud-streams-sample-sink</name>
|
||||
<description>Demo project for Spring XD module</description>
|
||||
|
||||
@@ -29,8 +26,8 @@
|
||||
<artifactId>spring-cloud-streams</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-messagebus-redis</artifactId>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-sample-source</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>spring-cloud-streams-sample-source</name>
|
||||
<description>Demo project for Spring XD module</description>
|
||||
|
||||
@@ -29,8 +26,8 @@
|
||||
<artifactId>spring-cloud-streams</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-messagebus-redis</artifactId>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-sample-tap</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>spring-cloud-streams-sample-tap</name>
|
||||
<description>Demo project for Spring XD module</description>
|
||||
|
||||
@@ -29,8 +26,8 @@
|
||||
<artifactId>spring-xd-runner</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-messagebus-redis</artifactId>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-sample-transform</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>spring-cloud-streams-sample-transform</name>
|
||||
@@ -29,8 +27,8 @@
|
||||
<artifactId>spring-cloud-streams</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-messagebus-redis</artifactId>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>spring-cloud-streams</name>
|
||||
<description>Messaging Microservices with Spring Integration</description>
|
||||
|
||||
@@ -19,7 +16,6 @@
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -53,20 +49,17 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-messagebus-local</artifactId>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-local</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-messagebus-redis</artifactId>
|
||||
<optional>true</optional>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.xd</groupId>
|
||||
<artifactId>spring-xd-messagebus-rabbit</artifactId>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-streams-binding-rabbit</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
||||
@@ -54,7 +54,6 @@ import org.springframework.xd.dirt.integration.bus.serializer.kryo.FileKryoRegis
|
||||
import org.springframework.xd.dirt.integration.bus.serializer.kryo.KryoRegistrar;
|
||||
import org.springframework.xd.dirt.integration.bus.serializer.kryo.PojoCodec;
|
||||
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @author David Turanski
|
||||
@@ -171,7 +170,14 @@ public class ChannelBindingAdapterConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnMissingBean(ChannelBindingProperties.class)
|
||||
protected static class ModulePropertiesConfiguration {
|
||||
@Bean(name = "spring.cloud.channels.CONFIGURATION_PROPERTIES")
|
||||
public ChannelBindingProperties moduleProperties() {
|
||||
return new ChannelBindingProperties();
|
||||
}
|
||||
}
|
||||
|
||||
protected static class CodecConfiguration {
|
||||
@Autowired
|
||||
ApplicationContext applicationContext;
|
||||
@@ -179,7 +185,7 @@ public class ChannelBindingAdapterConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "codec")
|
||||
public MultiTypeCodec<?> codec() {
|
||||
Map<String, KryoRegistrar> kryoRegistrarMap = this.applicationContext.getBeansOfType(KryoRegistrar
|
||||
Map<String, KryoRegistrar> kryoRegistrarMap = applicationContext.getBeansOfType(KryoRegistrar
|
||||
.class);
|
||||
return new PojoCodec(new ArrayList<>(kryoRegistrarMap.values()));
|
||||
}
|
||||
@@ -189,5 +195,4 @@ public class ChannelBindingAdapterConfiguration {
|
||||
return new FileKryoRegistrar();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ public class ModulePostProcessor implements BeanDefinitionRegistryPostProcessor,
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
|
||||
public void postProcessBeanDefinitionRegistry(final BeanDefinitionRegistry registry) throws BeansException {
|
||||
String[] beanDefinitionNames = registry.getBeanDefinitionNames();
|
||||
for (String beanDefinitionName : beanDefinitionNames) {
|
||||
BeanDefinition beanDefinition = registry.getBeanDefinition(beanDefinitionName);
|
||||
@@ -77,7 +77,7 @@ public class ModulePostProcessor implements BeanDefinitionRegistryPostProcessor,
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
|
||||
public Object postProcessBeforeInitialization(final Object bean, String beanName) throws BeansException {
|
||||
if (AnnotationUtils.findAnnotation(bean.getClass(), EnableModule.class) != null) {
|
||||
ReflectionUtils.doWithFields(bean.getClass(), new ReflectionUtils.FieldCallback() {
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Copyright 2013-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.xd.dirt.integration.bus;
|
||||
|
||||
import static org.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.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
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;
|
||||
import org.springframework.integration.support.utils.IntegrationUtils;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.scheduling.support.PeriodicTrigger;
|
||||
import org.springframework.xd.dirt.integration.bus.local.LocalMessageBus;
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @author Gary Russell
|
||||
*/
|
||||
public class MessageBusAwareChannelResolverTests {
|
||||
|
||||
private final StaticApplicationContext context = new StaticApplicationContext();
|
||||
|
||||
private volatile MessageBusAwareChannelResolver resolver;
|
||||
|
||||
private volatile LocalMessageBus bus;
|
||||
|
||||
@Before
|
||||
public void setupContext() throws Exception {
|
||||
this.bus = new LocalMessageBus();
|
||||
this.bus.setApplicationContext(context);
|
||||
this.bus.afterPropertiesSet();
|
||||
this.resolver = new MessageBusAwareChannelResolver(this.bus, null);
|
||||
this.resolver.setBeanFactory(context);
|
||||
context.getBeanFactory().registerSingleton("channelResolver",
|
||||
this.resolver);
|
||||
context.registerSingleton("other", DirectChannel.class);
|
||||
context.registerSingleton("taskScheduler", ThreadPoolTaskScheduler.class);
|
||||
context.registerSingleton(IntegrationUtils.INTEGRATION_MESSAGE_BUILDER_FACTORY_BEAN_NAME,
|
||||
DefaultMessageBuilderFactory.class);
|
||||
context.refresh();
|
||||
|
||||
PollerMetadata poller = new PollerMetadata();
|
||||
poller.setTrigger(new PeriodicTrigger(1000));
|
||||
bus.setPoller(poller);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveQueueChannel() {
|
||||
MessageChannel registered = resolver.resolveDestination("queue:foo");
|
||||
DirectChannel testChannel = new DirectChannel();
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
final List<Message<?>> received = new ArrayList<Message<?>>();
|
||||
testChannel.subscribe(new MessageHandler() {
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message<?> message) throws MessagingException {
|
||||
received.add(message);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
bus.bindConsumer("queue:foo", testChannel, 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(1, received.size());
|
||||
assertEquals("hello", received.get(0).getPayload());
|
||||
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<Message<?>> received = new ArrayList<Message<?>>();
|
||||
for (PublishSubscribeChannel testChannel : testChannels) {
|
||||
testChannel.subscribe(new MessageHandler() {
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message<?> message) throws MessagingException {
|
||||
received.add(message);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
bus.bindPubSubConsumer("topic:bar", testChannel, 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");
|
||||
assertSame(context.getBean("other"), other);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void propertyPassthrough() {
|
||||
Properties properties = new Properties();
|
||||
MessageBus bus = mock(MessageBus.class);
|
||||
doReturn(new DirectChannel()).when(bus).bindDynamicProducer("queue:foo", properties);
|
||||
doReturn(new DirectChannel()).when(bus).bindDynamicPubSubProducer("topic:bar", properties);
|
||||
MessageBusAwareChannelResolver resolver = new MessageBusAwareChannelResolver(bus, properties);
|
||||
BeanFactory beanFactory = new DefaultListableBeanFactory();
|
||||
resolver.setBeanFactory(beanFactory);
|
||||
resolver.resolveDestination("queue:foo");
|
||||
resolver.resolveDestination("topic:bar");
|
||||
verify(bus).bindDynamicProducer("queue:foo", properties);
|
||||
verify(bus).bindDynamicPubSubProducer("topic:bar", properties);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,11 +3,8 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-xd-runner</artifactId>
|
||||
<version>1.0.0.BUILD-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>spring-xd-runner</name>
|
||||
<description>Demo project for Spring XD Modules as apps</description>
|
||||
|
||||
@@ -19,7 +16,6 @@
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
Reference in New Issue
Block a user