|
19 | 19 | package org.apache.pulsar.metadata; |
20 | 20 |
|
21 | 21 | import static org.assertj.core.api.Assertions.assertThat; |
| 22 | +import static org.mockito.ArgumentMatchers.any; |
| 23 | +import static org.mockito.ArgumentMatchers.eq; |
| 24 | +import static org.mockito.Mockito.doAnswer; |
| 25 | +import static org.mockito.Mockito.mock; |
| 26 | +import static org.mockito.Mockito.times; |
| 27 | +import static org.mockito.Mockito.verify; |
22 | 28 | import static org.testng.Assert.assertEquals; |
23 | 29 | import static org.testng.Assert.assertFalse; |
24 | 30 | import static org.testng.Assert.assertNotNull; |
|
40 | 46 | import java.util.concurrent.BlockingQueue; |
41 | 47 | import java.util.concurrent.CompletableFuture; |
42 | 48 | import java.util.concurrent.CompletionException; |
| 49 | +import java.util.concurrent.CountDownLatch; |
43 | 50 | import java.util.concurrent.LinkedBlockingDeque; |
44 | 51 | import java.util.concurrent.TimeUnit; |
45 | 52 | import java.util.concurrent.atomic.AtomicInteger; |
|
48 | 55 | import lombok.Cleanup; |
49 | 56 | import lombok.SneakyThrows; |
50 | 57 | import lombok.extern.slf4j.Slf4j; |
| 58 | +import org.apache.bookkeeper.zookeeper.BoundExponentialBackoffRetryPolicy; |
51 | 59 | import org.apache.pulsar.common.util.FutureUtil; |
52 | 60 | import org.apache.pulsar.metadata.api.GetResult; |
53 | 61 | import org.apache.pulsar.metadata.api.MetadataStore; |
|
62 | 70 | import org.apache.pulsar.metadata.impl.PulsarZooKeeperClient; |
63 | 71 | import org.apache.pulsar.metadata.impl.ZKMetadataStore; |
64 | 72 | import org.apache.pulsar.metadata.impl.oxia.OxiaMetadataStore; |
| 73 | +import org.apache.zookeeper.AddWatchMode; |
| 74 | +import org.apache.zookeeper.AsyncCallback.VoidCallback; |
| 75 | +import org.apache.zookeeper.KeeperException; |
65 | 76 | import org.apache.zookeeper.WatchedEvent; |
66 | 77 | import org.apache.zookeeper.Watcher; |
67 | 78 | import org.apache.zookeeper.ZooKeeper; |
@@ -538,6 +549,53 @@ public void testZkLoadConfigFromFile() throws Exception { |
538 | 549 | assertFalse(zooKeeper.getClientConfig().isSaslClientEnabled()); |
539 | 550 | } |
540 | 551 |
|
| 552 | + @Test |
| 553 | + @SuppressWarnings("unchecked") |
| 554 | + public void testAsyncAddWatchRetriesWithWrapperCallback() throws Exception { |
| 555 | + String path = newKey(); |
| 556 | + @Cleanup |
| 557 | + PulsarZooKeeperClient zkClient = PulsarZooKeeperClient.newBuilder() |
| 558 | + .connectString(zks.getConnectionString()) |
| 559 | + .sessionTimeoutMs(3000) |
| 560 | + .operationRetryPolicy(new BoundExponentialBackoffRetryPolicy(0, 0, 3)) |
| 561 | + .build(); |
| 562 | + |
| 563 | + ZooKeeper mockZk = mock(ZooKeeper.class); |
| 564 | + AtomicInteger attempts = new AtomicInteger(); |
| 565 | + doAnswer(invocation -> { |
| 566 | + // The wrapper callback should consume this recoverable failure and retry the addWatch operation. |
| 567 | + int rc = attempts.incrementAndGet() == 1 |
| 568 | + ? KeeperException.Code.CONNECTIONLOSS.intValue() |
| 569 | + : KeeperException.Code.OK.intValue(); |
| 570 | + String callbackPath = invocation.getArgument(0); |
| 571 | + VoidCallback callback = invocation.getArgument(3); |
| 572 | + Object callbackContext = invocation.getArgument(4); |
| 573 | + callback.processResult(rc, callbackPath, callbackContext); |
| 574 | + return null; |
| 575 | + }).when(mockZk).addWatch(eq(path), any(Watcher.class), eq(AddWatchMode.PERSISTENT_RECURSIVE), |
| 576 | + any(VoidCallback.class), any()); |
| 577 | + |
| 578 | + // Force the Pulsar wrapper to delegate the async addWatch call to our controlled ZooKeeper instance. |
| 579 | + var zooKeeperRef = (AtomicReference<ZooKeeper>) WhiteboxImpl.getInternalState(zkClient, "zk"); |
| 580 | + zooKeeperRef.set(mockZk); |
| 581 | + |
| 582 | + CountDownLatch callbackCalled = new CountDownLatch(1); |
| 583 | + AtomicInteger callbackRc = new AtomicInteger(Integer.MIN_VALUE); |
| 584 | + zkClient.addWatch(path, event -> { |
| 585 | + }, AddWatchMode.PERSISTENT_RECURSIVE, (rc, callbackPath, ctx) -> { |
| 586 | + callbackRc.set(rc); |
| 587 | + callbackCalled.countDown(); |
| 588 | + }, null); |
| 589 | + |
| 590 | + assertTrue(callbackCalled.await(5, TimeUnit.SECONDS)); |
| 591 | + |
| 592 | + // The caller should only see the final successful result after the retry, not the first CONNECTIONLOSS. |
| 593 | + assertEquals(callbackRc.get(), KeeperException.Code.OK.intValue()); |
| 594 | + assertEquals(attempts.get(), 2); |
| 595 | + verify(mockZk, times(2)).addWatch(eq(path), any(Watcher.class), eq(AddWatchMode.PERSISTENT_RECURSIVE), |
| 596 | + any(VoidCallback.class), any()); |
| 597 | + } |
| 598 | + |
541 | 599 | @Test |
542 | 600 | public void testOxiaLoadConfigFromFile() throws Exception { |
543 | 601 | final String metadataStoreName = UUID.randomUUID().toString().replaceAll("-", ""); |
|
0 commit comments