From b66fa12907452b072d26990fb9d3b3b9cdde1dcd Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Wed, 25 Mar 2026 14:49:11 -0400 Subject: [PATCH] [bugfix] Fix BlobStoreImpl non-daemon threads causing CI test hangs BlobStoreImpl's PersistentWriter and BlobVacuum threads were created as non-daemon threads, which prevents JVM exit if BrokerPool shutdown fails to join them cleanly. This caused ~20% of CI runs to hang indefinitely after tests completed. Mark both threads as daemon threads so they cannot block JVM shutdown. The threads still participate in normal shutdown via poison pill (PersistentWriter) and interrupt (BlobVacuum), but now the JVM can exit even if those shutdown paths fail. Also add a forkedProcessExitTimeoutInSeconds safety net to the surefire configuration, and a regression test that verifies no non-daemon eXist threads survive BrokerPool shutdown. Follow-up to PR #6167 which fixed the same issue for StatusReporter. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../org/exist/storage/blob/BlobStoreImpl.java | 2 + .../exist/storage/BrokerPoolShutdownTest.java | 71 +++++++++++++++++++ exist-parent/pom.xml | 1 + 3 files changed, 74 insertions(+) create mode 100644 exist-core/src/test/java/org/exist/storage/BrokerPoolShutdownTest.java diff --git a/exist-core/src/main/java/org/exist/storage/blob/BlobStoreImpl.java b/exist-core/src/main/java/org/exist/storage/blob/BlobStoreImpl.java index 4cf9ec48bac..63ee512a258 100644 --- a/exist-core/src/main/java/org/exist/storage/blob/BlobStoreImpl.java +++ b/exist-core/src/main/java/org/exist/storage/blob/BlobStoreImpl.java @@ -327,12 +327,14 @@ public void open() throws IOException { this::abnormalPersistentWriterShutdown); this.persistentWriterThread = new Thread(blobStoreThreadGroup, persistentWriter, nameInstanceThread(database, "blob-store.persistent-writer")); + persistentWriterThread.setDaemon(true); persistentWriterThread.start(); // startup the blob vacuum thread this.blobVacuum = new BlobVacuum(vacuumQueue); this.blobVacuumThread = new Thread(blobStoreThreadGroup, blobVacuum, nameInstanceThread(database, "blob-store.vacuum")); + blobVacuumThread.setDaemon(true); blobVacuumThread.start(); // we are now open! diff --git a/exist-core/src/test/java/org/exist/storage/BrokerPoolShutdownTest.java b/exist-core/src/test/java/org/exist/storage/BrokerPoolShutdownTest.java new file mode 100644 index 00000000000..04bf310ac95 --- /dev/null +++ b/exist-core/src/test/java/org/exist/storage/BrokerPoolShutdownTest.java @@ -0,0 +1,71 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.storage; + +import org.exist.test.ExistEmbeddedServer; +import org.junit.Test; +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Verifies that BrokerPool shutdown doesn't leave non-daemon threads + * that would prevent JVM exit (cause of CI hangs). + */ +public class BrokerPoolShutdownTest { + + @Test + public void shutdownLeavesNoNonDaemonExistThreads() throws Exception { + // Start a BrokerPool + final ExistEmbeddedServer server = new ExistEmbeddedServer(true, true); + server.startDb(); + + // Shut it down + server.stopDb(); + + // Give threads a moment to terminate + Thread.sleep(2000); + + // Check for non-daemon eXist threads still alive + final Thread[] threads = new Thread[Thread.activeCount() * 2]; + Thread.enumerate(threads); + final List leaks = Arrays.stream(threads) + .filter(t -> t != null) + .filter(t -> !t.isDaemon()) + .filter(t -> t.isAlive()) + .filter(t -> { + final String name = t.getName().toLowerCase(); + return name.contains("exist") || name.contains("db.exist") + || name.contains("broker") || name.contains("blob"); + }) + .collect(Collectors.toList()); + + if (!leaks.isEmpty()) { + final String details = leaks.stream() + .map(t -> t.getName() + " (state=" + t.getState() + ")") + .collect(Collectors.joining(", ")); + fail("Non-daemon eXist threads survived shutdown: " + details); + } + } +} diff --git a/exist-parent/pom.xml b/exist-parent/pom.xml index 4c9650a180c..7a1a6094671 100644 --- a/exist-parent/pom.xml +++ b/exist-parent/pom.xml @@ -837,6 +837,7 @@ + 60 2C