|
34 | 34 | import java.util.HashMap; |
35 | 35 | import java.util.List; |
36 | 36 | import java.util.Map; |
| 37 | +import java.util.Optional; |
| 38 | +import java.util.concurrent.CountDownLatch; |
37 | 39 | import java.util.concurrent.ExecutionException; |
38 | 40 | import java.util.concurrent.ExecutorService; |
| 41 | +import java.util.concurrent.Executors; |
39 | 42 | import java.util.concurrent.ScheduledExecutorService; |
40 | 43 | import java.util.concurrent.TimeUnit; |
41 | 44 | import java.util.concurrent.TimeoutException; |
| 45 | +import java.util.concurrent.atomic.AtomicInteger; |
42 | 46 | import java.util.concurrent.atomic.AtomicLong; |
43 | 47 | import java.util.stream.Collectors; |
44 | 48 | import java.util.stream.IntStream; |
45 | 49 | import lombok.Cleanup; |
| 50 | +import lombok.extern.slf4j.Slf4j; |
| 51 | +import org.apache.logging.log4j.Level; |
46 | 52 | import org.apache.pulsar.broker.BrokerTestUtil; |
47 | 53 | import org.apache.pulsar.client.admin.PulsarAdminException; |
48 | 54 | import org.apache.pulsar.client.impl.ClientBuilderImpl; |
|
53 | 59 | import org.apache.pulsar.client.impl.conf.ClientConfigurationData; |
54 | 60 | import org.apache.pulsar.common.naming.TopicName; |
55 | 61 | import org.apache.pulsar.common.util.FutureUtil; |
| 62 | +import org.apache.pulsar.utils.TestLogAppender; |
56 | 63 | import org.awaitility.Awaitility; |
57 | 64 | import org.mockito.AdditionalAnswers; |
58 | 65 | import org.mockito.Mockito; |
|
62 | 69 | import org.testng.annotations.DataProvider; |
63 | 70 | import org.testng.annotations.Test; |
64 | 71 |
|
| 72 | +@Slf4j |
65 | 73 | @Test(groups = "broker") |
66 | 74 | public class MultiTopicsConsumerTest extends ProducerConsumerBase { |
67 | 75 | private ScheduledExecutorService internalExecutorServiceDelegate; |
@@ -431,4 +439,78 @@ public void testSubscriptionNotFound() throws PulsarAdminException, PulsarClient |
431 | 439 |
|
432 | 440 | pulsar.getConfiguration().setAllowAutoSubscriptionCreation(true); |
433 | 441 | } |
| 442 | + |
| 443 | + @Test(timeOut = 30000) |
| 444 | + public void testMessageListenerStopsProcessingAfterClosing() throws Exception { |
| 445 | + int numMessages = 100; |
| 446 | + String topic1 = newTopicName(); |
| 447 | + String topic2 = newTopicName(); |
| 448 | + final CountDownLatch consumerClosedLatch = new CountDownLatch(1); |
| 449 | + final CountDownLatch messageProcessedLatch = new CountDownLatch(1); |
| 450 | + AtomicInteger messageProcessedCount = new AtomicInteger(0); |
| 451 | + AtomicInteger messagesQueuedForExecutor = new AtomicInteger(0); |
| 452 | + AtomicInteger messagesCurrentlyInExecutor = new AtomicInteger(0); |
| 453 | + |
| 454 | + @Cleanup("shutdownNow") |
| 455 | + ExecutorService executor = Executors.newSingleThreadExecutor(); |
| 456 | + @Cleanup |
| 457 | + Consumer<byte[]> consumer = pulsarClient.newConsumer() |
| 458 | + .topics(List.of(topic1, topic2)) |
| 459 | + .subscriptionName("my-subscriber-name") |
| 460 | + .messageListenerExecutor(new MessageListenerExecutor() { |
| 461 | + @Override |
| 462 | + public void execute(Message<?> message, Runnable runnable) { |
| 463 | + messagesQueuedForExecutor.incrementAndGet(); |
| 464 | + messagesCurrentlyInExecutor.incrementAndGet(); |
| 465 | + executor.execute(() -> { |
| 466 | + try { |
| 467 | + runnable.run(); |
| 468 | + } finally { |
| 469 | + messagesCurrentlyInExecutor.decrementAndGet(); |
| 470 | + } |
| 471 | + }); |
| 472 | + } |
| 473 | + }) |
| 474 | + .messageListener((c1, msg) -> { |
| 475 | + messageProcessedCount.incrementAndGet(); |
| 476 | + c1.acknowledgeAsync(msg); |
| 477 | + messageProcessedLatch.countDown(); |
| 478 | + try { |
| 479 | + consumerClosedLatch.await(); |
| 480 | + } catch (InterruptedException e) { |
| 481 | + Thread.currentThread().interrupt(); |
| 482 | + throw new RuntimeException(e); |
| 483 | + } |
| 484 | + }).subscribe(); |
| 485 | + |
| 486 | + @Cleanup |
| 487 | + Producer<byte[]> producer = pulsarClient.newProducer() |
| 488 | + .topic(topic1) |
| 489 | + .enableBatching(false) |
| 490 | + .create(); |
| 491 | + |
| 492 | + for (int i = 0; i < numMessages; i++) { |
| 493 | + final String message = "my-message-" + i; |
| 494 | + producer.send(message.getBytes()); |
| 495 | + } |
| 496 | + |
| 497 | + assertTrue(messageProcessedLatch.await(5, TimeUnit.SECONDS)); |
| 498 | + // wait until all messages have been queued in the listener |
| 499 | + Awaitility.await().untilAsserted(() -> assertEquals(messagesQueuedForExecutor.get(), numMessages)); |
| 500 | + @Cleanup |
| 501 | + TestLogAppender testLogAppender = TestLogAppender.create(Optional.empty()); |
| 502 | + consumer.close(); |
| 503 | + consumerClosedLatch.countDown(); |
| 504 | + // only a single message should be processed |
| 505 | + assertEquals(messageProcessedCount.get(), 1); |
| 506 | + // wait until all messages have been drained from the executor |
| 507 | + Awaitility.await().untilAsserted(() -> assertEquals(messagesCurrentlyInExecutor.get(), 0)); |
| 508 | + testLogAppender.getEvents().forEach(logEvent -> { |
| 509 | + if (logEvent.getLevel() == Level.ERROR) { |
| 510 | + org.apache.logging.log4j.message.Message logEventMessage = logEvent.getMessage(); |
| 511 | + fail("No error should be logged when closing a consumer. Got: " + logEventMessage |
| 512 | + .getFormattedMessage() + " throwable:" + logEventMessage.getThrowable()); |
| 513 | + } |
| 514 | + }); |
| 515 | + } |
434 | 516 | } |
0 commit comments