Skip to content

Commit 86f560a

Browse files
Fix executor shutdown race between tests
shutdownBackgroundExecutorAsync() now captures the executor reference before spawning the shutdown thread, so it only shuts down the intended executor — not a replacement created by resetBackgroundInitializationState(). resetBackgroundInitializationState() now always creates a fresh executor instead of conditionally reusing one that may have a pending async shutdown. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 933171f commit 86f560a

1 file changed

Lines changed: 22 additions & 18 deletions

File tree

iterableapi/src/main/java/com/iterable/iterableapi/IterableBackgroundInitializer.java

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -338,24 +338,25 @@ static void shutdownBackgroundExecutor() {
338338
* Used internally after initialization completes
339339
*/
340340
private static void shutdownBackgroundExecutorAsync() {
341-
// Schedule shutdown on a separate thread to avoid blocking the executor thread
341+
// Capture the current executor reference so the shutdown thread only shuts down
342+
// THIS executor, not a replacement created by resetBackgroundInitializationState().
343+
final ExecutorService executorToShutdown = backgroundExecutor;
344+
if (executorToShutdown == null || executorToShutdown.isShutdown()) {
345+
return;
346+
}
342347
new Thread(() -> {
343-
synchronized (initLock) {
344-
if (backgroundExecutor != null && !backgroundExecutor.isShutdown()) {
345-
backgroundExecutor.shutdown();
346-
try {
347-
if (!backgroundExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
348-
IterableLogger.w(TAG, "Background executor did not terminate gracefully, forcing shutdown");
349-
backgroundExecutor.shutdownNow();
350-
}
351-
} catch (InterruptedException e) {
352-
IterableLogger.w(TAG, "Interrupted while waiting for executor termination");
353-
backgroundExecutor.shutdownNow();
354-
Thread.currentThread().interrupt();
355-
}
356-
IterableLogger.d(TAG, "Background executor shutdown completed");
348+
try {
349+
executorToShutdown.shutdown();
350+
if (!executorToShutdown.awaitTermination(5, TimeUnit.SECONDS)) {
351+
IterableLogger.w(TAG, "Background executor did not terminate gracefully, forcing shutdown");
352+
executorToShutdown.shutdownNow();
357353
}
354+
} catch (InterruptedException e) {
355+
IterableLogger.w(TAG, "Interrupted while waiting for executor termination");
356+
executorToShutdown.shutdownNow();
357+
Thread.currentThread().interrupt();
358358
}
359+
IterableLogger.d(TAG, "Background executor shutdown completed");
359360
}, "IterableExecutorShutdown").start();
360361
}
361362

@@ -413,10 +414,13 @@ static void resetBackgroundInitializationState() {
413414
pendingCallbacks.clear();
414415
callbackManager.reset();
415416

416-
// Recreate executor if it was shut down
417-
if (backgroundExecutor == null || backgroundExecutor.isShutdown()) {
418-
backgroundExecutor = createExecutor();
417+
// Always create a fresh executor. The old one may have a pending
418+
// shutdownBackgroundExecutorAsync that hasn't run yet — if we kept it,
419+
// the async shutdown would kill the executor under the next test.
420+
if (backgroundExecutor != null && !backgroundExecutor.isShutdown()) {
421+
backgroundExecutor.shutdownNow();
419422
}
423+
backgroundExecutor = createExecutor();
420424
}
421425
}
422426

0 commit comments

Comments
 (0)