/**
 * @author Mark Fisher
 * @author Gunnar Hillert
 * @author Gary Russell
 */
public class MessageListenerContainerMultipleQueueIntegrationTests {

  private static Log logger =
      LogFactory.getLog(MessageListenerContainerMultipleQueueIntegrationTests.class);

  private static Queue queue1 = new Queue("test.queue.1");

  private static Queue queue2 = new Queue("test.queue.2");

  @Rule
  public BrokerRunning brokerIsRunning =
      BrokerRunning.isRunningWithEmptyQueues(queue1.getName(), queue2.getName());

  @Rule
  public LogLevelAdjuster logLevels =
      new LogLevelAdjuster(
          Level.INFO,
          RabbitTemplate.class,
          SimpleMessageListenerContainer.class,
          BlockingQueueConsumer.class);

  @Rule public ExpectedException exception = ExpectedException.none();

  @After
  public void tearDown() {
    this.brokerIsRunning.removeTestQueues();
  }

  @Test
  public void testMultipleQueues() {
    doTest(1, container -> container.setQueues(queue1, queue2));
  }

  @Test
  public void testMultipleQueueNames() {
    doTest(1, container -> container.setQueueNames(queue1.getName(), queue2.getName()));
  }

  @Test
  public void testMultipleQueuesWithConcurrentConsumers() {
    doTest(3, container -> container.setQueues(queue1, queue2));
  }

  @Test
  public void testMultipleQueueNamesWithConcurrentConsumers() {
    doTest(3, container -> container.setQueueNames(queue1.getName(), queue2.getName()));
  }

  private void doTest(int concurrentConsumers, ContainerConfigurer configurer) {
    int messageCount = 10;
    RabbitTemplate template = new RabbitTemplate();
    CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
    connectionFactory.setHost("localhost");
    connectionFactory.setChannelCacheSize(concurrentConsumers);
    connectionFactory.setPort(BrokerTestUtils.getPort());
    template.setConnectionFactory(connectionFactory);
    SimpleMessageConverter messageConverter = new SimpleMessageConverter();
    messageConverter.setCreateMessageIds(true);
    template.setMessageConverter(messageConverter);
    for (int i = 0; i < messageCount; i++) {
      template.convertAndSend(queue1.getName(), new Integer(i));
      template.convertAndSend(queue2.getName(), new Integer(i));
    }
    final SimpleMessageListenerContainer container =
        new SimpleMessageListenerContainer(connectionFactory);
    final CountDownLatch latch = new CountDownLatch(messageCount * 2);
    PojoListener listener = new PojoListener(latch);
    container.setMessageListener(new MessageListenerAdapter(listener));
    container.setAcknowledgeMode(AcknowledgeMode.AUTO);
    container.setChannelTransacted(true);
    container.setConcurrentConsumers(concurrentConsumers);
    configurer.configure(container);
    container.afterPropertiesSet();
    container.start();
    try {
      int timeout = Math.min(1 + messageCount / concurrentConsumers, 30);
      boolean waited = latch.await(timeout, TimeUnit.SECONDS);
      logger.info("All messages recovered: " + waited);
      assertEquals(concurrentConsumers, container.getActiveConsumerCount());
      assertTrue("Timed out waiting for messages", waited);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      throw new IllegalStateException("unexpected interruption");
    } finally {
      container.shutdown();
      assertEquals(0, container.getActiveConsumerCount());
    }
    assertNull(template.receiveAndConvert(queue1.getName()));
    assertNull(template.receiveAndConvert(queue2.getName()));

    connectionFactory.destroy();
  }

  @FunctionalInterface
  private interface ContainerConfigurer {
    void configure(SimpleMessageListenerContainer container);
  }

  @SuppressWarnings("unused")
  private static class PojoListener {

    private final AtomicInteger count = new AtomicInteger();

    private final CountDownLatch latch;

    PojoListener(CountDownLatch latch) {
      this.latch = latch;
    }

    public void handleMessage(int value) throws Exception {
      logger.debug(value + ":" + count.getAndIncrement());
      latch.countDown();
    }

    public int getCount() {
      return count.get();
    }
  }
}
/**
 * @author Dave Syer
 * @author Gary Russell
 * @author Gunnar Hillert
 * @author Artem Bilan
 * @since 1.0
 */
public class MessageListenerContainerRetryIntegrationTests {

  private static Log logger =
      LogFactory.getLog(MessageListenerContainerRetryIntegrationTests.class);

  private static Queue queue = new Queue("test.queue");

  @Rule
  public BrokerRunning brokerIsRunning = BrokerRunning.isRunningWithEmptyQueues(queue.getName());

  @Rule
  public LogLevelAdjuster logLevels =
      new LogLevelAdjuster(
          Level.ERROR,
          RabbitTemplate.class,
          SimpleMessageListenerContainer.class,
          BlockingQueueConsumer.class);

  @Rule
  public LogLevelAdjuster traceLevels =
      new LogLevelAdjuster(
          Level.ERROR,
          StatefulRetryOperationsInterceptorFactoryBean.class,
          MessageListenerContainerRetryIntegrationTests.class);

  @Rule public ExpectedException exception = ExpectedException.none();

  @Rule public RepeatProcessor repeats = new RepeatProcessor();

  private RetryTemplate retryTemplate;

  private MessageConverter messageConverter;

  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);
    if (messageConverter == null) {
      SimpleMessageConverter messageConverter = new SimpleMessageConverter();
      messageConverter.setCreateMessageIds(true);
      this.messageConverter = messageConverter;
    }
    template.setMessageConverter(messageConverter);
    return template;
  }

  @After
  public void tearDown() {
    if (this.repeats.isFinalizing()) {
      this.brokerIsRunning.removeTestQueues();
    }
  }

  @Test
  public void testStatefulRetryWithAllMessagesFailing() throws Exception {

    int messageCount = 10;
    int txSize = 1;
    int failFrequency = 1;
    int concurrentConsumers = 3;
    doTestStatefulRetry(messageCount, txSize, failFrequency, concurrentConsumers);
  }

  @Test
  public void testStatelessRetryWithAllMessagesFailing() throws Exception {

    int messageCount = 10;
    int txSize = 1;
    int failFrequency = 1;
    int concurrentConsumers = 3;
    doTestStatelessRetry(messageCount, txSize, failFrequency, concurrentConsumers);
  }

  @Test
  public void testStatefulRetryWithNoMessageIds() throws Exception {

    int messageCount = 2;
    int txSize = 1;
    int failFrequency = 1;
    int concurrentConsumers = 1;
    SimpleMessageConverter messageConverter = new SimpleMessageConverter();
    // There will be no key for these messages so they cannot be recovered...
    messageConverter.setCreateMessageIds(false);
    this.messageConverter = messageConverter;
    // Beware of context cache busting if retry policy fails...
    this.retryTemplate = new RetryTemplate();
    this.retryTemplate.setRetryContextCache(new MapRetryContextCache(1));
    // The container should have shutdown, so there are now no active consumers
    exception.expectMessage("expected:<1> but was:<0>");
    doTestStatefulRetry(messageCount, txSize, failFrequency, concurrentConsumers);
  }

  @Test
  @Repeat(10)
  public void testStatefulRetryWithTxSizeAndIntermittentFailure() throws Exception {

    int messageCount = 10;
    int txSize = 4;
    int failFrequency = 3;
    int concurrentConsumers = 3;
    doTestStatefulRetry(messageCount, txSize, failFrequency, concurrentConsumers);
  }

  @Test
  public void testStatefulRetryWithMoreMessages() throws Exception {

    int messageCount = 200;
    int txSize = 10;
    int failFrequency = 6;
    int concurrentConsumers = 3;
    doTestStatefulRetry(messageCount, txSize, failFrequency, concurrentConsumers);
  }

  private Advice createRetryInterceptor(final CountDownLatch latch, boolean stateful)
      throws Exception {
    AbstractRetryOperationsInterceptorFactoryBean factory;
    if (stateful) {
      factory = new StatefulRetryOperationsInterceptorFactoryBean();
    } else {
      factory = new StatelessRetryOperationsInterceptorFactoryBean();
    }
    factory.setMessageRecoverer(
        (message, cause) -> {
          logger.warn(
              "Recovered: ["
                  + SerializationUtils.deserialize(message.getBody()).toString()
                  + "], message: "
                  + message);
          latch.countDown();
        });
    if (retryTemplate == null) {
      retryTemplate = new RetryTemplate();
    }
    factory.setRetryOperations(retryTemplate);
    return factory.getObject();
  }

  private void doTestStatefulRetry(
      int messageCount, int txSize, int failFrequency, int concurrentConsumers) throws Exception {
    doTestRetry(messageCount, txSize, failFrequency, concurrentConsumers, true);
  }

  private void doTestStatelessRetry(
      int messageCount, int txSize, int failFrequency, int concurrentConsumers) throws Exception {
    doTestRetry(messageCount, txSize, failFrequency, concurrentConsumers, false);
  }

  private void doTestRetry(
      int messageCount, int txSize, int failFrequency, int concurrentConsumers, boolean stateful)
      throws Exception {

    int failedMessageCount =
        messageCount / failFrequency + (messageCount % failFrequency == 0 ? 0 : 1);

    RabbitTemplate template = createTemplate(concurrentConsumers);
    for (int i = 0; i < messageCount; i++) {
      template.convertAndSend(queue.getName(), i);
    }

    final SimpleMessageListenerContainer container =
        new SimpleMessageListenerContainer(template.getConnectionFactory());
    PojoListener listener = new PojoListener(failFrequency);
    container.setMessageListener(new MessageListenerAdapter(listener));
    container.setAcknowledgeMode(AcknowledgeMode.AUTO);
    container.setChannelTransacted(true);
    container.setTxSize(txSize);
    container.setConcurrentConsumers(concurrentConsumers);

    final CountDownLatch latch = new CountDownLatch(failedMessageCount);
    container.setAdviceChain(new Advice[] {createRetryInterceptor(latch, stateful)});

    container.setQueueNames(queue.getName());
    container.afterPropertiesSet();
    container.start();

    try {

      int timeout = Math.min(1 + 2 * messageCount / concurrentConsumers, 30);

      final int count = messageCount;
      logger.debug("Waiting for messages with timeout = " + timeout + " (s)");
      Executors.newSingleThreadExecutor()
          .execute(
              () -> {
                while (container.getActiveConsumerCount() > 0) {
                  try {
                    Thread.sleep(100L);
                  } catch (InterruptedException e) {
                    latch.countDown();
                    Thread.currentThread().interrupt();
                    return;
                  }
                }
                for (int i = 0; i < count; i++) {
                  latch.countDown();
                }
              });
      boolean waited = latch.await(timeout, TimeUnit.SECONDS);
      logger.info("All messages recovered: " + waited);
      assertEquals(concurrentConsumers, container.getActiveConsumerCount());
      assertTrue("Timed out waiting for messages", waited);

      // Retried each failure 3 times (default retry policy)...
      assertEquals(3 * failedMessageCount, listener.getCount());

      // All failed messages recovered
      assertEquals(null, template.receiveAndConvert(queue.getName()));

    } finally {
      container.shutdown();
      ((DisposableBean) template.getConnectionFactory()).destroy();

      assertEquals(0, container.getActiveConsumerCount());
    }
  }

  private static class PojoListener {

    private final AtomicInteger count = new AtomicInteger();

    private final int failFrequency;

    PojoListener(int failFrequency) {
      this.failFrequency = failFrequency;
    }

    @SuppressWarnings("unused")
    public void handleMessage(int value) throws Exception {
      logger.debug("Handling: [" + value + "], fails:" + count);
      if (value % failFrequency == 0) {
        count.getAndIncrement();
        logger.debug("Failing: [" + value + "], fails:" + count);
        throw new RuntimeException("Planned");
      }
    }

    public int getCount() {
      return count.get();
    }
  }
}