/* * Copyright 2002-2017 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.amqp.rabbit.connection; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; 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.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.willReturn; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.logging.log4j.Level; import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; import org.springframework.amqp.AmqpAuthenticationException; import org.springframework.amqp.AmqpException; import org.springframework.amqp.AmqpIOException; import org.springframework.amqp.AmqpTimeoutException; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.CacheMode; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.junit.BrokerRunning; import org.springframework.amqp.rabbit.junit.BrokerTestUtils; import org.springframework.amqp.rabbit.test.LogLevelAdjuster; import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Recoverable; import com.rabbitmq.client.RecoveryListener; import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; import com.rabbitmq.client.impl.recovery.AutorecoveringConnection; /** * @author Dave Syer * @author Gunnar Hillert * @author Gary Russell * @author Artem Bilan * @since 1.0 * */ public class CachingConnectionFactoryIntegrationTests { private static final String CF_INTEGRATION_TEST_QUEUE = "cfIntegrationTest"; private static final String CF_INTEGRATION_CONNECTION_NAME = "cfIntegrationTestConnectionName"; private static Log logger = LogFactory.getLog(CachingConnectionFactoryIntegrationTests.class); private CachingConnectionFactory connectionFactory; @Rule public BrokerRunning brokerIsRunning = BrokerRunning.isRunningWithEmptyQueues(CF_INTEGRATION_TEST_QUEUE); @Rule public ExpectedException exception = ExpectedException.none(); @Rule public LogLevelAdjuster adjuster = new LogLevelAdjuster(Level.DEBUG, CachingConnectionFactoryIntegrationTests.class, CachingConnectionFactory.class) .categories("com.rabbitmq"); @Before public void open() { connectionFactory = new CachingConnectionFactory(); connectionFactory.setHost("localhost"); connectionFactory.setPort(BrokerTestUtils.getPort()); connectionFactory.getRabbitConnectionFactory().getClientProperties().put("foo", "bar"); connectionFactory.setConnectionNameStrategy(cf -> CF_INTEGRATION_CONNECTION_NAME); } @After public void close() { if (!this.connectionFactory.getVirtualHost().equals("non-existent")) { this.brokerIsRunning.removeTestQueues(); } assertEquals("bar", connectionFactory.getRabbitConnectionFactory().getClientProperties().get("foo")); connectionFactory.destroy(); } @Test public void testCachedConnections() { connectionFactory.setCacheMode(CacheMode.CONNECTION); connectionFactory.setConnectionCacheSize(5); connectionFactory.setExecutor(Executors.newCachedThreadPool()); List<Connection> connections = new ArrayList<>(); connections.add(connectionFactory.createConnection()); connections.add(connectionFactory.createConnection()); assertNotSame(connections.get(0), connections.get(1)); connections.add(connectionFactory.createConnection()); connections.add(connectionFactory.createConnection()); connections.add(connectionFactory.createConnection()); connections.add(connectionFactory.createConnection()); Set<?> allocatedConnections = TestUtils.getPropertyValue(connectionFactory, "allocatedConnections", Set.class); assertEquals(6, allocatedConnections.size()); connections.forEach(Connection::close); assertEquals(6, allocatedConnections.size()); assertEquals("5", connectionFactory.getCacheProperties().get("openConnections")); BlockingQueue<?> idleConnections = TestUtils.getPropertyValue(connectionFactory, "idleConnections", BlockingQueue.class); assertEquals(6, idleConnections.size()); connections.clear(); connections.add(connectionFactory.createConnection()); connections.add(connectionFactory.createConnection()); assertEquals(6, allocatedConnections.size()); assertEquals(4, idleConnections.size()); connections.forEach(Connection::close); } @Test public void testCachedConnectionsChannelLimit() throws Exception { connectionFactory.setCacheMode(CacheMode.CONNECTION); connectionFactory.setConnectionCacheSize(2); connectionFactory.setChannelCacheSize(1); connectionFactory.setChannelCheckoutTimeout(10); connectionFactory.setExecutor(Executors.newCachedThreadPool()); List<Connection> connections = new ArrayList<Connection>(); connections.add(connectionFactory.createConnection()); connections.add(connectionFactory.createConnection()); List<Channel> channels = new ArrayList<Channel>(); channels.add(connections.get(0).createChannel(false)); try { channels.add(connections.get(0).createChannel(false)); fail("Exception expected"); } catch (AmqpTimeoutException e) { } channels.add(connections.get(1).createChannel(false)); try { channels.add(connections.get(1).createChannel(false)); fail("Exception expected"); } catch (AmqpTimeoutException e) { } channels.get(0).close(); channels.get(1).close(); channels.add(connections.get(0).createChannel(false)); channels.add(connections.get(1).createChannel(false)); assertSame(channels.get(0), channels.get(2)); assertSame(channels.get(1), channels.get(3)); channels.get(2).close(); channels.get(3).close(); connections.forEach(Connection::close); } @Test public void testCachedConnectionsAndChannels() throws Exception { connectionFactory.setCacheMode(CacheMode.CONNECTION); connectionFactory.setConnectionCacheSize(1); connectionFactory.setChannelCacheSize(3); // the following is needed because we close the underlying connection below. connectionFactory.getRabbitConnectionFactory().setAutomaticRecoveryEnabled(false); List<Connection> connections = new ArrayList<Connection>(); connections.add(connectionFactory.createConnection()); connections.add(connectionFactory.createConnection()); Set<?> allocatedConnections = TestUtils.getPropertyValue(connectionFactory, "allocatedConnections", Set.class); assertEquals(2, allocatedConnections.size()); assertNotSame(connections.get(0), connections.get(1)); List<Channel> channels = new ArrayList<Channel>(); for (int i = 0; i < 5; i++) { channels.add(connections.get(0).createChannel(false)); channels.add(connections.get(1).createChannel(false)); channels.add(connections.get(0).createChannel(true)); channels.add(connections.get(1).createChannel(true)); } @SuppressWarnings("unchecked") Map<?, List<?>> cachedChannels = TestUtils.getPropertyValue(connectionFactory, "allocatedConnectionNonTransactionalChannels", Map.class); assertEquals(0, cachedChannels.get(connections.get(0)).size()); assertEquals(0, cachedChannels.get(connections.get(1)).size()); @SuppressWarnings("unchecked") Map<?, List<?>> cachedTxChannels = TestUtils.getPropertyValue(connectionFactory, "allocatedConnectionTransactionalChannels", Map.class); assertEquals(0, cachedTxChannels.get(connections.get(0)).size()); assertEquals(0, cachedTxChannels.get(connections.get(1)).size()); for (Channel channel : channels) { channel.close(); } assertEquals(3, cachedChannels.get(connections.get(0)).size()); assertEquals(3, cachedChannels.get(connections.get(1)).size()); assertEquals(3, cachedTxChannels.get(connections.get(0)).size()); assertEquals(3, cachedTxChannels.get(connections.get(1)).size()); for (int i = 0; i < 3; i++) { assertEquals(channels.get(i * 4), connections.get(0).createChannel(false)); assertEquals(channels.get(i * 4 + 1), connections.get(1).createChannel(false)); assertEquals(channels.get(i * 4 + 2), connections.get(0).createChannel(true)); assertEquals(channels.get(i * 4 + 3), connections.get(1).createChannel(true)); } assertEquals(0, cachedChannels.get(connections.get(0)).size()); assertEquals(0, cachedChannels.get(connections.get(1)).size()); assertEquals(0, cachedTxChannels.get(connections.get(0)).size()); assertEquals(0, cachedTxChannels.get(connections.get(1)).size()); for (Channel channel : channels) { channel.close(); } for (Connection connection : connections) { connection.close(); } assertEquals(3, cachedChannels.get(connections.get(0)).size()); assertEquals(0, cachedChannels.get(connections.get(1)).size()); assertEquals(3, cachedTxChannels.get(connections.get(0)).size()); assertEquals(0, cachedTxChannels.get(connections.get(1)).size()); assertEquals(2, allocatedConnections.size()); assertEquals("1", connectionFactory.getCacheProperties().get("openConnections")); Connection connection = connectionFactory.createConnection(); Connection rabbitConnection = TestUtils.getPropertyValue(connection, "target", Connection.class); rabbitConnection.close(); Channel channel = connection.createChannel(false); assertEquals(2, allocatedConnections.size()); assertEquals("1", connectionFactory.getCacheProperties().get("openConnections")); channel.close(); connection.close(); assertEquals(2, allocatedConnections.size()); assertEquals("1", connectionFactory.getCacheProperties().get("openConnections")); } @Test public void testSendAndReceiveFromVolatileQueue() throws Exception { RabbitTemplate template = new RabbitTemplate(connectionFactory); RabbitAdmin admin = new RabbitAdmin(connectionFactory); Queue queue = admin.declareQueue(); template.convertAndSend(queue.getName(), "message"); String result = (String) template.receiveAndConvert(queue.getName()); assertEquals("message", result); template.stop(); } @Test public void testReceiveFromNonExistentVirtualHost() throws Exception { connectionFactory.setVirtualHost("non-existent"); RabbitTemplate template = new RabbitTemplate(connectionFactory); // Wrong vhost is very unfriendly to client - the exception has no clue (just an EOF) exception.expect(anyOf(instanceOf(AmqpIOException.class), instanceOf(AmqpAuthenticationException.class))); template.receiveAndConvert("foo"); } @Test public void testSendAndReceiveFromVolatileQueueAfterImplicitRemoval() throws Exception { RabbitTemplate template = new RabbitTemplate(connectionFactory); RabbitAdmin admin = new RabbitAdmin(connectionFactory); Queue queue = admin.declareQueue(); template.convertAndSend(queue.getName(), "message"); // Force a physical close of the channel connectionFactory.destroy(); // The queue was removed when the channel was closed exception.expect(AmqpIOException.class); String result = (String) template.receiveAndConvert(queue.getName()); assertEquals("message", result); template.stop(); } @Test public void testMixTransactionalAndNonTransactional() throws Exception { RabbitTemplate template1 = new RabbitTemplate(connectionFactory); RabbitTemplate template2 = new RabbitTemplate(connectionFactory); template1.setChannelTransacted(true); RabbitAdmin admin = new RabbitAdmin(connectionFactory); Queue queue = admin.declareQueue(); template1.convertAndSend(queue.getName(), "message"); String result = (String) template2.receiveAndConvert(queue.getName()); assertEquals("message", result); // The channel is not transactional exception.expect(AmqpIOException.class); template2.execute(channel -> { // Should be an exception because the channel is not transactional channel.txRollback(); return null; }); } @Test public void testHardErrorAndReconnectNoAuto() throws Exception { this.connectionFactory.getRabbitConnectionFactory().setAutomaticRecoveryEnabled(false); RabbitTemplate template = new RabbitTemplate(connectionFactory); RabbitAdmin admin = new RabbitAdmin(connectionFactory); Queue queue = new Queue(CF_INTEGRATION_TEST_QUEUE); admin.declareQueue(queue); final String route = queue.getName(); final CountDownLatch latch = new CountDownLatch(1); try { template.execute(channel -> { channel.getConnection().addShutdownListener(cause -> { logger.info("Error", cause); latch.countDown(); // This will be thrown on the Connection thread just before it dies, so basically ignored throw new RuntimeException(cause); }); String tag = channel.basicConsume(route, new DefaultConsumer(channel)); // Consume twice with the same tag is a hard error (connection will be reset) String result = channel.basicConsume(route, false, tag, new DefaultConsumer(channel)); fail("Expected IOException, got: " + result); return null; }); fail("Expected AmqpIOException"); } catch (AmqpIOException e) { // expected } template.convertAndSend(route, "message"); assertTrue(latch.await(1000, TimeUnit.MILLISECONDS)); String result = (String) template.receiveAndConvert(route); assertEquals("message", result); result = (String) template.receiveAndConvert(route); assertEquals(null, result); } @Test public void testHardErrorAndReconnectAuto() throws Exception { this.connectionFactory.getRabbitConnectionFactory().setAutomaticRecoveryEnabled(true); Log cfLogger = spyOnLogger(this.connectionFactory); willReturn(true).given(cfLogger).isDebugEnabled(); RabbitTemplate template = new RabbitTemplate(connectionFactory); RabbitAdmin admin = new RabbitAdmin(connectionFactory); Queue queue = new Queue(CF_INTEGRATION_TEST_QUEUE); admin.declareQueue(queue); final String route = queue.getName(); final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch recoveryLatch = new CountDownLatch(1); final RecoveryListener channelRecoveryListener = new RecoveryListener() { @Override public void handleRecoveryStarted(Recoverable recoverable) { if (logger.isDebugEnabled()) { logger.debug("Channel recovery started: " + asString(recoverable)); } } @Override public void handleRecovery(Recoverable recoverable) { try { ((Channel) recoverable).basicCancel("testHardErrorAndReconnect"); } catch (IOException e) { } if (logger.isDebugEnabled()) { logger.debug("Channel recovery complete: " + asString(recoverable)); } } private String asString(Recoverable recoverable) { // TODO: https://github.com/rabbitmq/rabbitmq-java-client/issues/217 return ((AutorecoveringChannel) recoverable).getDelegate().toString(); } }; final RecoveryListener connectionRecoveryListener = new RecoveryListener() { @Override public void handleRecoveryStarted(Recoverable recoverable) { if (logger.isDebugEnabled()) { logger.debug("Connection recovery started: " + recoverable); } } @Override public void handleRecovery(Recoverable recoverable) { if (logger.isDebugEnabled()) { logger.debug("Connection recovery complete: " + recoverable); } recoveryLatch.countDown(); } }; Object connection = ((ConnectionProxy) this.connectionFactory.createConnection()).getTargetConnection(); connection = TestUtils.getPropertyValue(connection, "delegate"); if (connection instanceof AutorecoveringConnection) { ((AutorecoveringConnection) connection).addRecoveryListener(connectionRecoveryListener); } try { template.execute(channel -> { channel.getConnection().addShutdownListener(cause -> { logger.info("Error", cause); latch.countDown(); // This will be thrown on the Connection thread just before it dies, so basically ignored throw new RuntimeException(cause); }); Channel targetChannel = ((ChannelProxy) channel).getTargetChannel(); if (targetChannel instanceof AutorecoveringChannel) { ((AutorecoveringChannel) targetChannel).addRecoveryListener(channelRecoveryListener); } else { recoveryLatch.countDown(); // Spring IO Platform Tests } String tag = channel.basicConsume(route, false, "testHardErrorAndReconnect", new DefaultConsumer(channel)); // Consume twice with the same tag is a hard error (connection will be reset) String result = channel.basicConsume(route, false, tag, new DefaultConsumer(channel)); fail("Expected IOException, got: " + result); return null; }); fail("Expected AmqpIOException"); } catch (AmqpException e) { // expected } assertTrue(recoveryLatch.await(10, TimeUnit.SECONDS)); if (logger.isDebugEnabled()) { logger.debug("Resuming test after recovery complete"); } ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); verify(cfLogger, atLeastOnce()).debug(captor.capture()); assertThat(captor.getValue(), containsString("Connection recovery complete:")); template.convertAndSend(route, "message"); assertTrue(latch.await(10, TimeUnit.SECONDS)); String result = (String) template.receiveAndConvert(route); assertEquals("message", result); result = (String) template.receiveAndConvert(route); assertEquals(null, result); } @Test public void testConnectionCloseLog() { Log logger = spy(TestUtils.getPropertyValue(this.connectionFactory, "logger", Log.class)); new DirectFieldAccessor(this.connectionFactory).setPropertyValue("logger", logger); Connection conn = this.connectionFactory.createConnection(); conn.createChannel(false); this.connectionFactory.destroy(); verify(logger, never()).error(anyString()); } @Test public void testConnectionName() { Connection connection = this.connectionFactory.createConnection(); com.rabbitmq.client.Connection rabbitConnection = TestUtils.getPropertyValue(connection, "target.delegate", com.rabbitmq.client.Connection.class); assertEquals(CF_INTEGRATION_CONNECTION_NAME, rabbitConnection.getClientProperties().get("connection_name")); this.connectionFactory.destroy(); } @Test @Ignore // Don't run this on the CI build server public void hangOnClose() throws Exception { final Socket proxy = SocketFactory.getDefault().createSocket("localhost", 5672); final ServerSocket server = ServerSocketFactory.getDefault().createServerSocket(2765); final AtomicBoolean hangOnClose = new AtomicBoolean(); // create a simple proxy so we can drop the close response Executors.newSingleThreadExecutor().execute(() -> { try { final Socket socket = server.accept(); Executors.newSingleThreadExecutor().execute(() -> { while (!socket.isClosed()) { try { int c = socket.getInputStream().read(); if (c >= 0) { proxy.getOutputStream().write(c); } } catch (Exception e) { try { socket.close(); proxy.close(); } catch (Exception ee) { } } } }); while (!proxy.isClosed()) { try { int c = proxy.getInputStream().read(); if (c >= 0 && !hangOnClose.get()) { socket.getOutputStream().write(c); } } catch (Exception e) { try { socket.close(); proxy.close(); } catch (Exception ee) { } } } socket.close(); } catch (Exception e) { e.printStackTrace(); } }); CachingConnectionFactory factory = new CachingConnectionFactory(2765); factory.createConnection(); hangOnClose.set(true); factory.destroy(); } private Log spyOnLogger(CachingConnectionFactory connectionFactory2) { DirectFieldAccessor dfa = new DirectFieldAccessor(connectionFactory2); Log logger = spy((Log) dfa.getPropertyValue("logger")); dfa.setPropertyValue("logger", logger); return logger; } }