From e792b0e34726a8cbec532a1e568a4c6493befe46 Mon Sep 17 00:00:00 2001 From: Ilayaperumal Gopinathan Date: Tue, 19 Jan 2016 19:30:49 +0530 Subject: [PATCH] Use LocalizedQueueConnectionFactory from Spring AMQP - Remove LocalizedQueueConnectionFactory and the unit tests - The unit tests are already covered in Spring AMQP and also, certain methods accessors require public access to run tests from SCS - Kept the integration tests as is (though this can be removed as well - it is also covered in Spring AMQP) This resolves #279 --- .../LocalizedQueueConnectionFactory.java | 231 ------------------ .../rabbit/RabbitMessageChannelBinder.java | 1 + ...ueueConnectionFactoryIntegrationTests.java | 1 + .../LocalizedQueueConnectionFactoryTests.java | 186 -------------- 4 files changed, 2 insertions(+), 417 deletions(-) delete mode 100644 spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactory.java delete mode 100644 spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryTests.java diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactory.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactory.java deleted file mode 100644 index 7815bba3c..000000000 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactory.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.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; - - -/** - * 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. - *

{@link #getTargetConnectionFactory(Object)} is invoked by the - * {@code SimpleMessageListenerContainer}, when establishing a connection, with the lookup key having - * the format {@code '[queueName]'}. - *

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 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 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; - } - -} diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java index 8b7bd8fcd..adb0119b4 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/main/java/org/springframework/cloud/stream/binder/rabbit/RabbitMessageChannelBinder.java @@ -42,6 +42,7 @@ import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.TopicExchange; import org.springframework.amqp.rabbit.config.RetryInterceptorBuilder; import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.connection.LocalizedQueueConnectionFactory; import org.springframework.amqp.rabbit.core.BatchingRabbitTemplate; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryIntegrationTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryIntegrationTests.java index 2548e07b4..29f00cf48 100644 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryIntegrationTests.java +++ b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryIntegrationTests.java @@ -27,6 +27,7 @@ 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.connection.LocalizedQueueConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.cloud.stream.test.junit.rabbit.RabbitTestSupport; diff --git a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryTests.java b/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryTests.java deleted file mode 100644 index 929e9ce52..000000000 --- a/spring-cloud-stream-binders/spring-cloud-stream-binder-rabbit/src/test/java/org/springframework/cloud/stream/binder/rabbit/LocalizedQueueConnectionFactoryTests.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.stream.binder.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 cfs = new HashMap<>(); - - private final Map connections = new HashMap<>(); - - private final Map channels = new HashMap<>(); - - private final Map consumers = new HashMap<>(); - - private final Map 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 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 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() { - - @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; - } - -}