diff --git a/core/src/main/java/org/testcontainers/utility/ducttape/Timeouts.java b/core/src/main/java/org/testcontainers/utility/ducttape/Timeouts.java index 4433886b4f9..4a70340be2f 100644 --- a/core/src/main/java/org/testcontainers/utility/ducttape/Timeouts.java +++ b/core/src/main/java/org/testcontainers/utility/ducttape/Timeouts.java @@ -9,20 +9,28 @@ */ public class Timeouts { - private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool(new ThreadFactory() { + private static final AtomicInteger THREAD_COUNTER = new AtomicInteger(0); - final AtomicInteger threadCounter = new AtomicInteger(0); + private static final ThreadFactory THREAD_FACTORY = r -> { + Thread thread = new Thread(r, "ducttape-" + THREAD_COUNTER.getAndIncrement()); + thread.setDaemon(true); + return thread; + }; - @Override - public Thread newThread(Runnable r) { - Thread thread = new Thread(r, "ducttape-" + threadCounter.getAndIncrement()); - thread.setDaemon(true); - return thread; + private static volatile ExecutorService executorService; + + private static synchronized ExecutorService getExecutorService() { + if (executorService == null || executorService.isShutdown()) { + executorService = Executors.newCachedThreadPool(THREAD_FACTORY); } - }); + return executorService; + } - public static void shutdown() { - EXECUTOR_SERVICE.shutdown(); + public static synchronized void shutdown() { + if (executorService != null) { + executorService.shutdown(); + executorService = null; + } } /** @@ -40,7 +48,7 @@ public static T getWithTimeout(final int timeout, final TimeUnit timeUnit, f check("timeout must be greater than zero", timeout > 0); - Future future = EXECUTOR_SERVICE.submit(lambda); + Future future = getExecutorService().submit(lambda); return callFuture(timeout, timeUnit, future); } @@ -57,7 +65,7 @@ public static void doWithTimeout(final int timeout, final TimeUnit timeUnit, fin check("timeout must be greater than zero", timeout > 0); - Future future = EXECUTOR_SERVICE.submit(lambda); + Future future = getExecutorService().submit(lambda); callFuture(timeout, timeUnit, future); } diff --git a/core/src/test/java/org/testcontainers/utility/TimeoutsShutdownTest.java b/core/src/test/java/org/testcontainers/utility/TimeoutsShutdownTest.java new file mode 100644 index 00000000000..3ac7a28f6d5 --- /dev/null +++ b/core/src/test/java/org/testcontainers/utility/TimeoutsShutdownTest.java @@ -0,0 +1,35 @@ +package org.testcontainers.utility; + +import org.junit.Test; +import org.testcontainers.utility.ducttape.Timeouts; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@link Timeouts} works correctly across shutdown/reuse cycles. + * After {@code shutdown()} the executor is re-created on next use. + */ +public class TimeoutsShutdownTest { + + @Test + public void timeoutsWorkAfterShutdown() { + // First use + String result1 = Timeouts.getWithTimeout(5, TimeUnit.SECONDS, () -> "container-1-ready"); + assertThat(result1).isEqualTo("container-1-ready"); + + // Shutdown (as GenericContainer.stop() does) + Timeouts.shutdown(); + + // Second use — should transparently create a fresh executor + String result2 = Timeouts.getWithTimeout(5, TimeUnit.SECONDS, () -> "container-2-ready"); + assertThat(result2).isEqualTo("container-2-ready"); + + // Shutdown and use again to confirm repeatable + Timeouts.shutdown(); + + String result3 = Timeouts.getWithTimeout(5, TimeUnit.SECONDS, () -> "container-3-ready"); + assertThat(result3).isEqualTo("container-3-ready"); + } +}