From 1a54f45a73ca669479f67bbb7b5f8acbc976b13d Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Wed, 25 Mar 2026 21:53:48 -0400 Subject: [PATCH] [bugfix] Add bounded timeouts to BlobStore shutdown join() calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BlobStoreImpl.normalClose() and abnormalPersistentWriterShutdown() called Thread.join() with no timeout on the PersistentWriter and BlobVacuum threads. If either thread failed to terminate promptly, the shutdown would hang indefinitely — causing CI test suite timeouts. Add 30-second bounded timeouts to all join() calls. If the PersistentWriter doesn't terminate within 30s, it is interrupted and given 5 more seconds. The threads remain non-daemon to ensure pending blob writes complete during normal shutdown. This supersedes #6179, which incorrectly made BlobStore threads daemon threads. As @adamretter (author of BlobStore) pointed out, these threads must remain non-daemon to ensure pending blob writes complete. The fix is bounded join() timeouts during shutdown — giving threads 30s to drain before proceeding. Verified: 3/3 full test suite trials pass with clean exit (3:26-4:23), 27/27 BlobStore-specific tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../org/exist/storage/blob/BlobStoreImpl.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) 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..6a37db74221 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 @@ -447,13 +447,21 @@ private void normalClose() throws IOException { if (persistentWriter != null) { persistQueue.put(PersistentWriter.POISON_PILL); } - persistentWriterThread.join(); + persistentWriterThread.join(30_000); + if (persistentWriterThread.isAlive()) { + LOG.warn("BlobStore PersistentWriter did not terminate within 30s, interrupting"); + persistentWriterThread.interrupt(); + persistentWriterThread.join(5_000); + } // shutdown the vacuum if (blobVacuum != null) { blobVacuumThread.interrupt(); } - blobVacuumThread.join(); + blobVacuumThread.join(30_000); + if (blobVacuumThread.isAlive()) { + LOG.warn("BlobStore BlobVacuum did not terminate within 30s"); + } } catch (final InterruptedException e) { // Restore the interrupted status Thread.currentThread().interrupt(); @@ -523,7 +531,10 @@ private void abnormalPersistentWriterShutdown() { if (blobVacuum != null) { blobVacuumThread.interrupt(); } - blobVacuumThread.join(); + blobVacuumThread.join(30_000); + if (blobVacuumThread.isAlive()) { + LOG.warn("BlobStore BlobVacuum did not terminate within 30s during abnormal shutdown"); + } } catch (final InterruptedException e) { // Restore the interrupted status