You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pulsar.apache.org by lh...@apache.org on 2026/09/14 09:56:41 UTC
(pulsar) 12/15: [fix][test] Cover scheduler shutdown membership-lock deadlock (#26557)
This is an automated email from the ASF dual-hosted git repository.
lhotari pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/pulsar.git
commit 2a05e1dcdba0f84e7d45a25aa9147f9032b940d5
Author: Lari Hotari <lh...@users.noreply.github.com>
AuthorDate: Mon Sep 14 01:00:46 2026 +0300
[fix][test] Cover scheduler shutdown membership-lock deadlock (#26557)
(cherry picked from commit 6075c2b4ddf7fd89a2e08cec4b4b8fe6286ee0af)
---
pulsar-functions/worker/pom.xml | 6 +++
.../functions/worker/SchedulerManagerTest.java | 43 ++++++++++++++++++++++
2 files changed, 49 insertions(+)
diff --git a/pulsar-functions/worker/pom.xml b/pulsar-functions/worker/pom.xml
index 9daabb673f87..84da1acdb232 100644
--- a/pulsar-functions/worker/pom.xml
+++ b/pulsar-functions/worker/pom.xml
@@ -209,6 +209,12 @@
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ <scope>test</scope>
+ </dependency>
+
</dependencies>
<build>
diff --git a/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/SchedulerManagerTest.java b/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/SchedulerManagerTest.java
index 87d4e82450d3..076cae8d25f9 100644
--- a/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/SchedulerManagerTest.java
+++ b/pulsar-functions/worker/src/test/java/org/apache/pulsar/functions/worker/SchedulerManagerTest.java
@@ -26,11 +26,13 @@ import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertThrows;
import static org.testng.Assert.assertTrue;
import com.google.protobuf.InvalidProtocolBufferException;
import io.netty.util.concurrent.DefaultThreadFactory;
@@ -44,12 +46,15 @@ import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantLock;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.pulsar.client.admin.LongRunningProcessStatus;
@@ -67,6 +72,7 @@ import org.apache.pulsar.functions.runtime.thread.ThreadRuntimeFactory;
import org.apache.pulsar.functions.runtime.thread.ThreadRuntimeFactoryConfig;
import org.apache.pulsar.functions.utils.FunctionCommon;
import org.apache.pulsar.functions.worker.scheduler.RoundRobinScheduler;
+import org.awaitility.Awaitility;
import org.mockito.Mockito;
import org.mockito.invocation.Invocation;
import org.testng.Assert;
@@ -155,6 +161,43 @@ public class SchedulerManagerTest {
this.executor.shutdownNow();
}
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testCloseDoesNotBlockMembershipChecksWhileWaitingForScheduler() throws Exception {
+ doReturn(List.of(WorkerInfo.of("worker-1", "localhost", 5000)))
+ .when(membershipManager).getCurrentMembership();
+ schedulerManager.initialize(producer);
+ ReentrantLock schedulerLock = (ReentrantLock) schedulerManager.getSchedulerLock();
+ AtomicReference<Thread> closingThread = new AtomicReference<>();
+ ExecutorService closeExecutor = Executors.newSingleThreadExecutor(new DefaultThreadFactory("scheduler-close"));
+ Future<?> closeFuture = null;
+ schedulerLock.lock();
+ try {
+ closeFuture = closeExecutor.submit(() -> {
+ closingThread.set(Thread.currentThread());
+ schedulerManager.close();
+ });
+ Awaitility.await().until(() -> closingThread.get() != null
+ && schedulerLock.hasQueuedThread(closingThread.get()));
+
+ // A scheduler holding schedulerLock must still be able to read membership while close waits.
+ // Use a separate task with a bounded wait so a regression can release the lock during cleanup.
+ executor.submit(() -> assertThrows(SchedulerManager.TooFewWorkersException.class,
+ () -> schedulerManager.rebalanceIfNotInprogress())).get(5, TimeUnit.SECONDS);
+ verify(producer, never()).close();
+ } finally {
+ schedulerLock.unlock();
+ try {
+ if (closeFuture != null) {
+ closeFuture.get(5, TimeUnit.SECONDS);
+ }
+ } finally {
+ closeExecutor.shutdownNow();
+ }
+ }
+ verify(producer).close();
+ }
+
@Test
public void testSchedule() throws Exception {