/* * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * 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.listener; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; 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.AtomicInteger; 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.Rule; import org.junit.Test; import org.mockito.Mockito; import org.springframework.amqp.AmqpIllegalStateException; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.MessageListener; 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.RabbitTemplate; import org.springframework.amqp.rabbit.junit.BrokerRunning; import org.springframework.amqp.rabbit.junit.BrokerTestUtils; import org.springframework.amqp.rabbit.junit.LongRunningIntegrationTest; import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; import org.springframework.amqp.rabbit.listener.exception.FatalListenerStartupException; import org.springframework.amqp.rabbit.test.LogLevelAdjuster; import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.DisposableBean; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author Dave Syer * @author Gary Russell * @author Gunnar Hillert * @author Artem Bilan * @since 1.0 * */ public class MessageListenerContainerLifecycleIntegrationTests { private static Log logger = LogFactory.getLog(MessageListenerContainerLifecycleIntegrationTests.class); private static Queue queue = new Queue("test.queue"); private enum TransactionMode { ON, OFF, PREFETCH, PREFETCH_NO_TX; public boolean isTransactional() { return this != OFF && this != PREFETCH_NO_TX; } public AcknowledgeMode getAcknowledgeMode() { return this == OFF ? AcknowledgeMode.NONE : AcknowledgeMode.AUTO; } public int getPrefetch() { return this == PREFETCH || this == PREFETCH_NO_TX ? 10 : -1; } public int getTxSize() { return this == PREFETCH || this == PREFETCH_NO_TX ? 5 : -1; } } private enum Concurrency { LOW(1), HIGH(5); private final int value; Concurrency(int value) { this.value = value; } public int value() { return this.value; } } private enum MessageCount { LOW(1), MEDIUM(20), HIGH(500); private final int value; MessageCount(int value) { this.value = value; } public int value() { return this.value; } } @Rule public LongRunningIntegrationTest longTests = new LongRunningIntegrationTest(); @Rule public BrokerRunning brokerIsRunning = BrokerRunning.isRunningWithEmptyQueues(queue.getName()); @Rule public LogLevelAdjuster logLevels = new LogLevelAdjuster(Level.INFO, RabbitTemplate.class, SimpleMessageListenerContainer.class, BlockingQueueConsumer.class, MessageListenerContainerLifecycleIntegrationTests.class); private RabbitTemplate createTemplate(int concurrentConsumers) { RabbitTemplate template = new RabbitTemplate(); CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); connectionFactory.setHost("localhost"); connectionFactory.setChannelCacheSize(concurrentConsumers); connectionFactory.setPort(BrokerTestUtils.getPort()); template.setConnectionFactory(connectionFactory); return template; } @After public void tearDown() { this.brokerIsRunning.removeTestQueues(); } @Test public void testTransactionalLowLevel() throws Exception { doTest(MessageCount.MEDIUM, Concurrency.LOW, TransactionMode.ON); } @Test public void testTransactionalHighLevel() throws Exception { doTest(MessageCount.HIGH, Concurrency.HIGH, TransactionMode.ON); } @Test public void testTransactionalLowLevelWithPrefetch() throws Exception { doTest(MessageCount.MEDIUM, Concurrency.LOW, TransactionMode.PREFETCH); } @Test public void testTransactionalHighLevelWithPrefetch() throws Exception { doTest(MessageCount.HIGH, Concurrency.HIGH, TransactionMode.PREFETCH); } @Test public void testNonTransactionalLowLevel() throws Exception { doTest(MessageCount.MEDIUM, Concurrency.LOW, TransactionMode.OFF); } @Test public void testNonTransactionalHighLevel() throws Exception { doTest(MessageCount.HIGH, Concurrency.HIGH, TransactionMode.OFF); } @Test public void testNonTransactionalLowLevelWithPrefetch() throws Exception { doTest(MessageCount.MEDIUM, Concurrency.LOW, TransactionMode.PREFETCH_NO_TX); } @Test public void testNonTransactionalHighLevelWithPrefetch() throws Exception { doTest(MessageCount.HIGH, Concurrency.HIGH, TransactionMode.PREFETCH_NO_TX); } @Test public void testBadCredentials() throws Exception { RabbitTemplate template = createTemplate(1); com.rabbitmq.client.ConnectionFactory cf = new com.rabbitmq.client.ConnectionFactory(); cf.setUsername("foo"); final CachingConnectionFactory connectionFactory = new CachingConnectionFactory(cf); try { this.doTest(MessageCount.LOW, Concurrency.LOW, TransactionMode.OFF, template, connectionFactory); fail("expected exception"); } catch (AmqpIllegalStateException e) { assertTrue("Expected FatalListenerStartupException", e.getCause() instanceof FatalListenerStartupException); } catch (Throwable t) { fail("expected FatalListenerStartupException:" + t.getClass() + ":" + t.getMessage()); } ((DisposableBean) template.getConnectionFactory()).destroy(); } private void doTest(MessageCount level, Concurrency concurrency, TransactionMode transactionMode) throws Exception { RabbitTemplate template = createTemplate(concurrency.value); this.doTest(level, concurrency, transactionMode, template, template.getConnectionFactory()); } /** * If transactionMode is OFF, the undelivered messages will be lost (ack=NONE). If it is * ON, PREFETCH, or PREFETCH_NO_TX, ack=AUTO, so we should not lose any messages. */ private void doTest(MessageCount level, Concurrency concurrency, TransactionMode transactionMode, RabbitTemplate template, ConnectionFactory connectionFactory) throws Exception { int messageCount = level.value(); int concurrentConsumers = concurrency.value(); boolean transactional = transactionMode.isTransactional(); CountDownLatch latch = new CountDownLatch(messageCount); for (int i = 0; i < messageCount; i++) { template.convertAndSend(queue.getName(), i + "foo"); } SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); PojoListener listener = new PojoListener(latch); container.setMessageListener(new MessageListenerAdapter(listener)); container.setAcknowledgeMode(transactionMode.getAcknowledgeMode()); container.setChannelTransacted(transactionMode.isTransactional()); container.setConcurrentConsumers(concurrentConsumers); if (transactionMode.getPrefetch() > 0) { container.setPrefetchCount(transactionMode.getPrefetch()); container.setTxSize(transactionMode.getTxSize()); } container.setQueueNames(queue.getName()); container.setShutdownTimeout(30000); container.afterPropertiesSet(); container.start(); try { boolean waited = latch.await(50, TimeUnit.MILLISECONDS); logger.info("All messages received before stop: " + waited); if (messageCount > 1) { assertFalse("Expected not to receive all messages before stop", waited); } assertEquals(concurrentConsumers, container.getActiveConsumerCount()); container.stop(); int n = 0; while (n++ < 100 && container.getActiveConsumerCount() > 0) { Thread.sleep(100); } assertEquals(0, container.getActiveConsumerCount()); if (!transactional) { int messagesReceivedAfterStop = listener.getCount(); boolean prefetchNoTx = transactionMode == TransactionMode.PREFETCH_NO_TX; waited = latch.await(prefetchNoTx ? 100 : 10000, TimeUnit.MILLISECONDS); // AMQP-338 logger.info("All messages received after stop: " + waited + " (" + messagesReceivedAfterStop + ")"); if (prefetchNoTx) { assertFalse("Didn't expect to receive all messages after stop", waited); } else { assertTrue("Expect to receive all messages after stop", waited); } assertEquals("Unexpected additional messages received after stop", messagesReceivedAfterStop, listener.getCount()); for (int i = 0; i < messageCount; i++) { template.convertAndSend(queue.getName(), i + "bar"); } // Even though not transactional, we shouldn't lose messages for PREFETCH_NO_TX int expectedAfterRestart = transactionMode == TransactionMode.PREFETCH_NO_TX ? messageCount * 2 - messagesReceivedAfterStop : messageCount; latch = new CountDownLatch(expectedAfterRestart); listener.reset(latch); } int messagesReceivedBeforeStart = listener.getCount(); container.start(); int timeout = Math.min(1 + messageCount / (4 * concurrentConsumers), 30); logger.debug("Waiting for messages with timeout = " + timeout + " (s)"); waited = latch.await(timeout, TimeUnit.SECONDS); logger.info("All messages received after start: " + waited); assertEquals(concurrentConsumers, container.getActiveConsumerCount()); if (transactional) { assertTrue("Timed out waiting for message", waited); } else { int count = listener.getCount(); assertTrue("Expected additional messages received after start: " + messagesReceivedBeforeStart + ">=" + count, messagesReceivedBeforeStart < count); assertNull("Messages still available", template.receive(queue.getName())); } assertEquals(concurrentConsumers, container.getActiveConsumerCount()); } finally { container.shutdown(); } int n = 0; while (n++ < 100 && container.getActiveConsumerCount() > 0) { Thread.sleep(100); } assertEquals(0, container.getActiveConsumerCount()); assertNull(template.receiveAndConvert(queue.getName())); ((DisposableBean) template.getConnectionFactory()).destroy(); } /* * Tests that only prefetch is processed after stop(). */ @Test public void testShutDownWithPrefetch() throws Exception { int messageCount = 10; int concurrentConsumers = 1; RabbitTemplate template = createTemplate(concurrentConsumers); for (int i = 0; i < messageCount; i++) { template.convertAndSend(queue.getName(), i + "foo"); } final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(template.getConnectionFactory()); final CountDownLatch prefetched = new CountDownLatch(1); final CountDownLatch awaitStart1 = new CountDownLatch(1); final CountDownLatch awaitStart2 = new CountDownLatch(6); final CountDownLatch awaitStop = new CountDownLatch(1); final AtomicInteger received = new AtomicInteger(); final CountDownLatch awaitConsumeFirst = new CountDownLatch(5); final CountDownLatch awaitConsumeSecond = new CountDownLatch(10); container.setMessageListener((MessageListener) message -> { try { awaitStart1.countDown(); prefetched.await(10, TimeUnit.SECONDS); awaitStart2.countDown(); awaitStop.await(10, TimeUnit.SECONDS); received.incrementAndGet(); awaitConsumeFirst.countDown(); awaitConsumeSecond.countDown(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); container.setAcknowledgeMode(AcknowledgeMode.AUTO); container.setConcurrentConsumers(concurrentConsumers); container.setPrefetchCount(5); container.setQueueNames(queue.getName()); container.afterPropertiesSet(); container.start(); // wait until the listener has the first message... assertTrue(awaitStart1.await(10, TimeUnit.SECONDS)); // ... and the remaining 4 are queued... @SuppressWarnings("unchecked") Set<BlockingQueueConsumer> consumers = (Set<BlockingQueueConsumer>) TestUtils .getPropertyValue(container, "consumers"); int n = 0; while (n++ < 100) { if (consumers.size() > 0) { if (TestUtils.getPropertyValue(consumers.iterator().next(), "queue", BlockingQueue.class) .size() > 3) { prefetched.countDown(); break; } } Thread.sleep(100); } Executors.newSingleThreadExecutor().execute(() -> container.stop()); n = 0; while (container.isActive() && n++ < 100) { Thread.sleep(100); } assertTrue(n < 100); awaitStop.countDown(); assertTrue("awaitConsumeFirst.count=" + awaitConsumeFirst.getCount(), awaitConsumeFirst.await(10, TimeUnit.SECONDS)); n = 0; DirectFieldAccessor dfa = new DirectFieldAccessor(container); while (dfa.getPropertyValue("consumers") != null && n++ < 100) { Thread.sleep(100); } assertTrue(n < 100); // make sure we stopped receiving after the prefetch was consumed assertEquals(5, received.get()); assertEquals(1, awaitStart2.getCount()); container.start(); assertTrue(awaitStart2.await(10, TimeUnit.SECONDS)); assertTrue("awaitConsumeSecond.count=" + awaitConsumeSecond.getCount(), awaitConsumeSecond.await(10, TimeUnit.SECONDS)); container.stop(); ((DisposableBean) template.getConnectionFactory()).destroy(); } @Test public void testSimpleMessageListenerContainerStoppedWithoutWarn() throws Exception { CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); connectionFactory.setHost("localhost"); connectionFactory.setPort(BrokerTestUtils.getPort()); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); Log log = spy(TestUtils.getPropertyValue(container, "logger", Log.class)); final CountDownLatch latch = new CountDownLatch(1); when(log.isDebugEnabled()).thenReturn(true); doAnswer(invocation -> { latch.countDown(); invocation.callRealMethod(); return null; }).when(log).debug( Mockito.contains("Consumer received Shutdown Signal, processing stopped")); DirectFieldAccessor dfa = new DirectFieldAccessor(container); dfa.setPropertyValue("logger", log); container.setQueues(queue); container.setMessageListener(new MessageListenerAdapter()); container.afterPropertiesSet(); container.start(); try { connectionFactory.destroy(); assertTrue(latch.await(10, TimeUnit.SECONDS)); Mockito.verify(log).debug( Mockito.contains("Consumer received Shutdown Signal, processing stopped")); Mockito.verify(log, Mockito.never()).warn(Mockito.anyString(), Mockito.any(Throwable.class)); } finally { container.stop(); connectionFactory.destroy(); } } // AMQP-496 @Test public void testLongLivingConsumerStoppedProperlyAfterContextClose() throws Exception { ConfigurableApplicationContext applicationContext = new AnnotationConfigApplicationContext(LongLiveConsumerConfig.class); RabbitTemplate template = createTemplate(1); template.convertAndSend(queue.getName(), "foo"); CountDownLatch consumerLatch = applicationContext.getBean("consumerLatch", CountDownLatch.class); SimpleMessageListenerContainer container = applicationContext.getBean(SimpleMessageListenerContainer.class); assertTrue(consumerLatch.await(10, TimeUnit.SECONDS)); applicationContext.close(); @SuppressWarnings("rawtypes") ActiveObjectCounter counter = TestUtils.getPropertyValue(container, "cancellationLock", ActiveObjectCounter.class); assertTrue(counter.getCount() > 0); int n = 0; while (counter.getCount() > 0 && n++ < 10) { Thread.sleep(500); } assertTrue(n < 10); ((DisposableBean) template.getConnectionFactory()).destroy(); } @Configuration static class LongLiveConsumerConfig { @Bean public CountDownLatch consumerLatch() { return new CountDownLatch(1); } @Bean public ConnectionFactory connectionFactory() { CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); connectionFactory.setHost("localhost"); connectionFactory.setPort(BrokerTestUtils.getPort()); return connectionFactory; } @Bean public SimpleMessageListenerContainer container() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory()); container.setQueues(queue); container.setMessageListener((MessageListener) message -> { try { consumerLatch().countDown(); Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); container.setShutdownTimeout(1); return container; } } public static class PojoListener { private final AtomicInteger count = new AtomicInteger(); private CountDownLatch latch; public PojoListener(CountDownLatch latch) { this.latch = latch; } public void reset(CountDownLatch latch) { this.latch = latch; } public void handleMessage(String value) throws Exception { try { logger.debug(value + count.getAndIncrement()); Thread.sleep(10); } finally { latch.countDown(); } } public int getCount() { return count.get(); } } }