This is an automated email from the ASF dual-hosted git repository.
wenbingshen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/bookkeeper.git
The following commit(s) were added to refs/heads/master by this push:
new a41c70061d Issue 4797:Prevent thread leak when BookKeeper client
constructor fails (#4798)
a41c70061d is described below
commit a41c70061df0bb822af321c6d61c8c525ce7bb88
Author: KROOS <[email protected]>
AuthorDate: Thu Aug 13 15:40:07 2026 +0800
Issue 4797:Prevent thread leak when BookKeeper client constructor fails
(#4798)
* [FIX] Prevent thread leak when BookKeeper client constructor fails
* add test
* fix checkstyle
* add close
* use close directly
* use Try-finally to handle the close.
---------
Co-authored-by: shanxu <[email protected]>
---
.../org/apache/bookkeeper/client/BookKeeper.java | 287 ++++++++++++---------
.../client/BookKeeperConstructorFailureTest.java | 176 +++++++++++++
2 files changed, 335 insertions(+), 128 deletions(-)
diff --git
a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/BookKeeper.java
b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/BookKeeper.java
index db34b482f2..1542cdcaf3 100644
---
a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/BookKeeper.java
+++
b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/BookKeeper.java
@@ -423,127 +423,144 @@ public class BookKeeper implements
org.apache.bookkeeper.client.api.BookKeeper {
this.internalConf =
ClientInternalConf.fromConfigAndFeatureProvider(conf, this.featureProvider);
- // initialize resources
- this.scheduler =
OrderedScheduler.newSchedulerBuilder().numThreads(1).name("BookKeeperClientScheduler").build();
- this.highPriorityTaskExecutor =
-
OrderedScheduler.newSchedulerBuilder().numThreads(1).name("BookKeeperHighPriorityThread").build();
- this.mainWorkerPool = OrderedExecutor.newBuilder()
- .name("BookKeeperClientWorker")
- .numThreads(conf.getNumWorkerThreads())
- .statsLogger(rootStatsLogger)
- .traceTaskExecution(conf.getEnableTaskExecutionStats())
-
.preserveMdcForTaskExecution(conf.getPreserveMdcForTaskExecution())
-
.traceTaskWarnTimeMicroSec(conf.getTaskExecutionWarnTimeMicros())
- .enableBusyWait(conf.isBusyWaitEnabled())
- .build();
-
- // initialize stats logger
- this.statsLogger =
rootStatsLogger.scope(BookKeeperClientStats.CLIENT_SCOPE);
- this.clientStats = BookKeeperClientStats.newInstance(this.statsLogger);
-
- // initialize metadata driver
+ boolean initialized = false;
try {
- String metadataServiceUriStr = conf.getMetadataServiceUri();
- if (null != metadataServiceUriStr) {
- this.metadataDriver =
MetadataDrivers.getClientDriver(URI.create(metadataServiceUriStr));
- } else {
- checkNotNull(zkc, "No external zookeeper provided when no
metadata service uri is found");
- this.metadataDriver = MetadataDrivers.getClientDriver("zk");
+ // initialize resources
+ this.scheduler =
+
OrderedScheduler.newSchedulerBuilder().numThreads(1).name("BookKeeperClientScheduler").build();
+ this.highPriorityTaskExecutor =
+
OrderedScheduler.newSchedulerBuilder().numThreads(1).name("BookKeeperHighPriorityThread").build();
+ this.mainWorkerPool = OrderedExecutor.newBuilder()
+ .name("BookKeeperClientWorker")
+ .numThreads(conf.getNumWorkerThreads())
+ .statsLogger(rootStatsLogger)
+ .traceTaskExecution(conf.getEnableTaskExecutionStats())
+
.preserveMdcForTaskExecution(conf.getPreserveMdcForTaskExecution())
+
.traceTaskWarnTimeMicroSec(conf.getTaskExecutionWarnTimeMicros())
+ .enableBusyWait(conf.isBusyWaitEnabled())
+ .build();
+
+ // initialize stats logger
+ this.statsLogger =
rootStatsLogger.scope(BookKeeperClientStats.CLIENT_SCOPE);
+ this.clientStats =
BookKeeperClientStats.newInstance(this.statsLogger);
+
+ // initialize metadata driver
+ try {
+ String metadataServiceUriStr = conf.getMetadataServiceUri();
+ if (null != metadataServiceUriStr) {
+ this.metadataDriver =
MetadataDrivers.getClientDriver(URI.create(metadataServiceUriStr));
+ } else {
+ checkNotNull(zkc, "No external zookeeper provided when no
metadata service uri is found");
+ this.metadataDriver =
MetadataDrivers.getClientDriver("zk");
+ }
+ this.metadataDriver.initialize(
+ conf,
+ highPriorityTaskExecutor,
+ rootStatsLogger,
+ Optional.ofNullable(zkc));
+ } catch (ConfigurationException ce) {
+ log.error()
+ .exception(ce)
+ .log("Failed to initialize metadata client driver
using invalid metadata service uri");
+ throw new IOException("Failed to initialize metadata client
driver", ce);
+ } catch (MetadataException me) {
+ log.error().exception(me).log("Encountered metadata exceptions
on initializing metadata client driver");
+ throw new IOException("Failed to initialize metadata client
driver", me);
}
- this.metadataDriver.initialize(
- conf,
- highPriorityTaskExecutor,
- rootStatsLogger,
- Optional.ofNullable(zkc));
- } catch (ConfigurationException ce) {
- log.error()
- .exception(ce)
- .log("Failed to initialize metadata client driver using
invalid metadata service uri");
- throw new IOException("Failed to initialize metadata client
driver", ce);
- } catch (MetadataException me) {
- log.error().exception(me).log("Encountered metadata exceptions on
initializing metadata client driver");
- throw new IOException("Failed to initialize metadata client
driver", me);
- }
- // initialize event loop group
- if (null == eventLoopGroup) {
- this.eventLoopGroup = EventLoopUtil.getClientEventLoopGroup(conf,
- new DefaultThreadFactory("bookkeeper-io"));
- this.ownEventLoopGroup = true;
- } else {
- this.eventLoopGroup = eventLoopGroup;
- this.ownEventLoopGroup = false;
- }
+ // initialize event loop group
+ if (null == eventLoopGroup) {
+ this.eventLoopGroup =
EventLoopUtil.getClientEventLoopGroup(conf,
+ new DefaultThreadFactory("bookkeeper-io"));
+ this.ownEventLoopGroup = true;
+ } else {
+ this.eventLoopGroup = eventLoopGroup;
+ this.ownEventLoopGroup = false;
+ }
- if (byteBufAllocator != null) {
- this.allocator = byteBufAllocator;
- } else {
- this.allocator = ByteBufAllocatorBuilder.create()
- .poolingPolicy(conf.getAllocatorPoolingPolicy())
- .poolingConcurrency(conf.getAllocatorPoolingConcurrency())
- .outOfMemoryPolicy(conf.getAllocatorOutOfMemoryPolicy())
-
.leakDetectionPolicy(conf.getAllocatorLeakDetectionPolicy())
- .exitOnOutOfMemory(conf.exitOnOutOfMemory())
- .build();
- }
+ if (byteBufAllocator != null) {
+ this.allocator = byteBufAllocator;
+ } else {
+ this.allocator = ByteBufAllocatorBuilder.create()
+ .poolingPolicy(conf.getAllocatorPoolingPolicy())
+
.poolingConcurrency(conf.getAllocatorPoolingConcurrency())
+
.outOfMemoryPolicy(conf.getAllocatorOutOfMemoryPolicy())
+
.leakDetectionPolicy(conf.getAllocatorLeakDetectionPolicy())
+ .exitOnOutOfMemory(conf.exitOnOutOfMemory())
+ .build();
+ }
+ if (null == requestTimer) {
+ this.requestTimer = new HashedWheelTimer(
+ new
ThreadFactoryBuilder().setNameFormat("BookieClientTimer-%d").build(),
+ conf.getTimeoutTimerTickDurationMs(),
TimeUnit.MILLISECONDS,
+ conf.getTimeoutTimerNumTicks());
+ this.ownTimer = true;
+ } else {
+ this.requestTimer = requestTimer;
+ this.ownTimer = false;
+ }
- if (null == requestTimer) {
- this.requestTimer = new HashedWheelTimer(
- new
ThreadFactoryBuilder().setNameFormat("BookieClientTimer-%d").build(),
- conf.getTimeoutTimerTickDurationMs(),
TimeUnit.MILLISECONDS,
- conf.getTimeoutTimerNumTicks());
- this.ownTimer = true;
- } else {
- this.requestTimer = requestTimer;
- this.ownTimer = false;
- }
+ BookieAddressResolver bookieAddressResolver =
conf.getBookieAddressResolverEnabled()
+ ? new
DefaultBookieAddressResolver(metadataDriver.getRegistrationClient())
+ : new BookieAddressResolverDisabled();
+ if (dnsResolver != null) {
+ dnsResolver.setBookieAddressResolver(bookieAddressResolver);
+ }
+ // initialize the ensemble placement
+ this.placementPolicy = initializeEnsemblePlacementPolicy(conf,
+ dnsResolver, this.requestTimer, this.featureProvider,
this.statsLogger, bookieAddressResolver);
+
+ this.bookieWatcher = new BookieWatcherImpl(
+ conf, this.placementPolicy,
metadataDriver.getRegistrationClient(), bookieAddressResolver,
+ this.statsLogger.scope(WATCHER_SCOPE));
+
+ // initialize bookie client
+ this.bookieClient = new BookieClientImpl(conf,
this.eventLoopGroup, this.allocator, this.mainWorkerPool,
+ scheduler, rootStatsLogger,
this.bookieWatcher.getBookieAddressResolver());
+
+ if (conf.getDiskWeightBasedPlacementEnabled()) {
+ log.info("Weighted ledger placement enabled");
+ ThreadFactoryBuilder tFBuilder = new ThreadFactoryBuilder()
+ .setNameFormat("BKClientMetaDataPollScheduler-%d");
+ this.bookieInfoScheduler =
Executors.newSingleThreadScheduledExecutor(tFBuilder.build());
+ this.bookieInfoReader = new BookieInfoReader(this, conf,
this.bookieInfoScheduler);
+ this.bookieWatcher.initialBlockingBookieRead();
+ this.bookieInfoReader.start();
+ } else {
+ log.info("Weighted ledger placement is not enabled");
+ this.bookieInfoScheduler = null;
+ this.bookieInfoReader = new BookieInfoReader(this, conf, null);
+ this.bookieWatcher.initialBlockingBookieRead();
+ }
- BookieAddressResolver bookieAddressResolver =
conf.getBookieAddressResolverEnabled()
- ? new
DefaultBookieAddressResolver(metadataDriver.getRegistrationClient())
- : new BookieAddressResolverDisabled();
- if (dnsResolver != null) {
- dnsResolver.setBookieAddressResolver(bookieAddressResolver);
- }
- // initialize the ensemble placement
- this.placementPolicy = initializeEnsemblePlacementPolicy(conf,
- dnsResolver, this.requestTimer, this.featureProvider,
this.statsLogger, bookieAddressResolver);
-
- this.bookieWatcher = new BookieWatcherImpl(
- conf, this.placementPolicy,
metadataDriver.getRegistrationClient(), bookieAddressResolver,
- this.statsLogger.scope(WATCHER_SCOPE));
-
- // initialize bookie client
- this.bookieClient = new BookieClientImpl(conf, this.eventLoopGroup,
this.allocator, this.mainWorkerPool,
- scheduler, rootStatsLogger,
this.bookieWatcher.getBookieAddressResolver());
-
- if (conf.getDiskWeightBasedPlacementEnabled()) {
- log.info("Weighted ledger placement enabled");
- ThreadFactoryBuilder tFBuilder = new ThreadFactoryBuilder()
- .setNameFormat("BKClientMetaDataPollScheduler-%d");
- this.bookieInfoScheduler =
Executors.newSingleThreadScheduledExecutor(tFBuilder.build());
- this.bookieInfoReader = new BookieInfoReader(this, conf,
this.bookieInfoScheduler);
- this.bookieWatcher.initialBlockingBookieRead();
- this.bookieInfoReader.start();
- } else {
- log.info("Weighted ledger placement is not enabled");
- this.bookieInfoScheduler = null;
- this.bookieInfoReader = new BookieInfoReader(this, conf, null);
- this.bookieWatcher.initialBlockingBookieRead();
- }
+ // initialize ledger manager
+ try {
+ this.ledgerManagerFactory =
+ this.metadataDriver.getLedgerManagerFactory();
+ } catch (MetadataException e) {
+ throw new IOException("Failed to initialize ledger manager
factory", e);
+ }
+ this.ledgerManager = new
CleanupLedgerManager(ledgerManagerFactory.newLedgerManager());
+ this.ledgerIdGenerator =
ledgerManagerFactory.newLedgerIdGenerator();
- // initialize ledger manager
- try {
- this.ledgerManagerFactory =
- this.metadataDriver.getLedgerManagerFactory();
- } catch (MetadataException e) {
- throw new IOException("Failed to initialize ledger manager
factory", e);
+ this.bookieQuarantineRatio = conf.getBookieQuarantineRatio();
+ scheduleBookieHealthCheckIfEnabled(conf);
+ initialized = true;
+ } finally {
+ if (!initialized) {
+ try {
+ close();
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ log.warn().exception(ie)
+ .log("Interrupted while closing
partially-initialized BookKeeper client");
+ } catch (Throwable t) {
+ log.warn().exception(t)
+ .log("Failed to close partially-initialized
BookKeeper client");
+ }
+ }
}
- this.ledgerManager = new
CleanupLedgerManager(ledgerManagerFactory.newLedgerManager());
- this.ledgerIdGenerator = ledgerManagerFactory.newLedgerIdGenerator();
-
- this.bookieQuarantineRatio = conf.getBookieQuarantineRatio();
- scheduleBookieHealthCheckIfEnabled(conf);
}
/**
@@ -1520,32 +1537,44 @@ public class BookKeeper implements
org.apache.bookkeeper.client.api.BookKeeper {
// Close bookie client so all pending bookie requests would be failed
// which will reject any incoming bookie requests.
- bookieClient.close();
+ if (bookieClient != null) {
+ bookieClient.close();
+ }
try {
// Close ledger manage so all pending metadata requests would be
failed
// which will reject any incoming metadata requests.
- ledgerManager.close();
- ledgerIdGenerator.close();
+ if (ledgerManager != null) {
+ ledgerManager.close();
+ }
+ if (ledgerIdGenerator != null) {
+ ledgerIdGenerator.close();
+ }
} catch (IOException ie) {
log.error().exception(ie).log("Failed to close ledger manager");
}
// Close the scheduler
- scheduler.shutdown();
- if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
- log.warn("The scheduler did not shutdown cleanly");
+ if (scheduler != null) {
+ scheduler.shutdown();
+ if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
+ log.warn("The scheduler did not shutdown cleanly");
+ }
}
// Close the watchTask scheduler
- highPriorityTaskExecutor.shutdown();
- if (!highPriorityTaskExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
- log.warn("The highPriorityTaskExecutor for WatchTask did not
shutdown cleanly, interrupting");
- highPriorityTaskExecutor.shutdownNow();
+ if (highPriorityTaskExecutor != null) {
+ highPriorityTaskExecutor.shutdown();
+ if (!highPriorityTaskExecutor.awaitTermination(10,
TimeUnit.SECONDS)) {
+ log.warn("The highPriorityTaskExecutor for WatchTask did not
shutdown cleanly, interrupting");
+ highPriorityTaskExecutor.shutdownNow();
+ }
}
- mainWorkerPool.shutdown();
- if (!mainWorkerPool.awaitTermination(10, TimeUnit.SECONDS)) {
- log.warn("The mainWorkerPool did not shutdown cleanly");
+ if (mainWorkerPool != null) {
+ mainWorkerPool.shutdown();
+ if (!mainWorkerPool.awaitTermination(10, TimeUnit.SECONDS)) {
+ log.warn("The mainWorkerPool did not shutdown cleanly");
+ }
}
if (this.bookieInfoScheduler != null) {
this.bookieInfoScheduler.shutdown();
@@ -1554,13 +1583,15 @@ public class BookKeeper implements
org.apache.bookkeeper.client.api.BookKeeper {
}
}
- if (ownTimer) {
+ if (ownTimer && requestTimer != null) {
requestTimer.stop();
}
- if (ownEventLoopGroup) {
+ if (ownEventLoopGroup && eventLoopGroup != null) {
eventLoopGroup.shutdownGracefully();
}
- this.metadataDriver.close();
+ if (metadataDriver != null) {
+ this.metadataDriver.close();
+ }
}
@Override
diff --git
a/bookkeeper-server/src/test/java/org/apache/bookkeeper/client/BookKeeperConstructorFailureTest.java
b/bookkeeper-server/src/test/java/org/apache/bookkeeper/client/BookKeeperConstructorFailureTest.java
new file mode 100644
index 0000000000..2d5db8658b
--- /dev/null
+++
b/bookkeeper-server/src/test/java/org/apache/bookkeeper/client/BookKeeperConstructorFailureTest.java
@@ -0,0 +1,176 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.bookkeeper.client;
+
+import static org.junit.Assert.fail;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.TreeMap;
+import org.apache.bookkeeper.conf.ClientConfiguration;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Verifies that when {@link BookKeeper} construction fails at metadata-driver
+ * initialization, the {@link
org.apache.bookkeeper.common.util.OrderedExecutor}
+ * pools allocated before that point are released and do not accumulate across
+ * repeated failing {@code build()} calls.
+ *
+ * <p>This test targets the <em>early-failure</em> path (an unreachable
+ * ZooKeeper URI), which is the most common production misconfiguration and the
+ * historical source of thread leaks. It does not cover resources allocated
after
+ * the metadata driver (e.g. {@code eventLoopGroup}, {@code requestTimer},
+ * {@code bookieInfoScheduler}); those code paths require a reachable ZK and
are
+ * exercised by {@link BookKeeperCloseTest} together with the construction
+ * cleanup logic in {@link BookKeeper}.
+ *
+ * <p>Every {@code build()} attempt runs inside a dedicated {@link ThreadGroup}
+ * and only threads living in that group are counted, so concurrently executing
+ * tests in the same JVM cannot pollute the result.
+ */
+public class BookKeeperConstructorFailureTest {
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(BookKeeperConstructorFailureTest.class);
+
+ private static final String UNREACHABLE_METADATA_URI =
"zk://127.0.0.1:1/ledgers";
+
+ /**
+ * Thread-name prefixes of the three OrderedExecutor pools that BookKeeper
allocates
+ * <em>before</em> {@code metadataDriver.initialize(...)}. With an
unreachable ZK URI,
+ * construction fails inside that call, so only these three pools can
possibly leak
+ * on this code path.
+ */
+ private static final String[] CLIENT_THREAD_PREFIXES = new String[] {
+ "BookKeeperClientScheduler",
+ "BookKeeperClientWorker",
+ "BookKeeperHighPriorityThread",
+ };
+
+ private static final int FAILED_BUILD_ITERATIONS = 5;
+
+ private static final long THREAD_SHUTDOWN_TIMEOUT_MS = 5_000L;
+
+ /** Repeated failing {@code build()} calls must not accumulate worker-pool
threads. */
+ @Test
+ public void testRepeatedFailedBuildsDoNotAccumulateThreads() throws
Exception {
+ ThreadGroup group = new ThreadGroup("bk-ctor-failure-test");
+ runFailingBuildsIn(group);
+ waitForThreadShutdown(group, THREAD_SHUTDOWN_TIMEOUT_MS);
+
+ Map<String, Integer> leak = snapshotClientThreadCounts(group);
+ if (hasLeak(leak)) {
+ LOG.error("Thread leak detected after {} failed build()s in group
{}: {}",
+ FAILED_BUILD_ITERATIONS, group.getName(), leak);
+ fail("BookKeeper constructor leaked threads after " +
FAILED_BUILD_ITERATIONS
+ + " failed build()s in group " + group.getName() + ": " +
leak);
+ }
+ }
+
+ private static void runFailingBuildsIn(ThreadGroup group) throws
InterruptedException {
+ ClientConfiguration conf = newFailingClientConf();
+ Throwable[] driverError = new Throwable[1];
+ Thread driver = new Thread(group, () -> {
+ for (int i = 0; i < FAILED_BUILD_ITERATIONS; i++) {
+ try {
+ BookKeeper.forConfig(conf).build();
+ driverError[0] = new AssertionError(
+ "BookKeeper construction should have failed at
iteration " + i);
+ return;
+ } catch (Exception expected) {
+ LOG.debug("iteration {} failed as expected: {}", i,
expected.toString());
+ }
+ }
+ }, "bk-ctor-failure-driver");
+ driver.start();
+ driver.join();
+ if (driverError[0] instanceof AssertionError) {
+ throw (AssertionError) driverError[0];
+ }
+ }
+
+ // ---------- helpers ----------
+
+ private static ClientConfiguration newFailingClientConf() {
+ ClientConfiguration conf = new ClientConfiguration();
+ conf.setMetadataServiceUri(UNREACHABLE_METADATA_URI);
+ conf.setZkTimeout(1000);
+ conf.setZkRetryBackoffMaxRetries(0);
+ return conf;
+ }
+
+ /** Returns the live-thread counts inside {@code group} grouped by {@link
#CLIENT_THREAD_PREFIXES}. */
+ private static Map<String, Integer> snapshotClientThreadCounts(ThreadGroup
group) {
+ Thread[] threads = new Thread[Math.max(16, group.activeCount() * 2)];
+ int n;
+ // Loop to handle the race where a thread starts between activeCount()
and enumerate().
+ while ((n = group.enumerate(threads, true)) == threads.length) {
+ threads = new Thread[threads.length * 2];
+ }
+ Map<String, Integer> counts = new TreeMap<>();
+ for (String prefix : CLIENT_THREAD_PREFIXES) {
+ counts.put(prefix, 0);
+ }
+ for (int i = 0; i < n; i++) {
+ Thread t = threads[i];
+ if (t == null || !t.isAlive()) {
+ continue;
+ }
+ String prefix = matchPrefix(t.getName());
+ if (prefix != null) {
+ counts.merge(prefix, 1, Integer::sum);
+ }
+ }
+ return counts;
+ }
+
+ private static String matchPrefix(String name) {
+ if (name == null) {
+ return null;
+ }
+ return Arrays.stream(CLIENT_THREAD_PREFIXES)
+ .filter(name::startsWith)
+ .findFirst()
+ .orElse(null);
+ }
+
+ private static boolean hasLeak(Map<String, Integer> counts) {
+ for (Integer v : counts.values()) {
+ if (v != null && v > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static void waitForThreadShutdown(ThreadGroup group, long
timeoutMs)
+ throws InterruptedException {
+ long deadline = System.currentTimeMillis() + timeoutMs;
+ while (System.currentTimeMillis() < deadline) {
+ if (!hasLeak(snapshotClientThreadCounts(group))) {
+ return;
+ }
+ Thread.sleep(50);
+ }
+ }
+}
\ No newline at end of file