From 5c3899004d9c5430d4f152edfa1de2d0dfb7983a Mon Sep 17 00:00:00 2001 From: David Pilato Date: Tue, 17 Sep 2024 00:48:18 +0200 Subject: [PATCH 1/4] [Bug]: duct-tape:1.0.8 contains a Thread Leak 1st step: move code from the archived repository Related to #9227. --- core/build.gradle | 3 - .../containers/GenericContainer.java | 2 +- .../startupcheck/StartupCheckStrategy.java | 6 +- .../wait/strategy/AbstractWaitStrategy.java | 4 +- .../DockerHealthcheckWaitStrategy.java | 4 +- .../wait/strategy/HttpWaitStrategy.java | 4 +- .../wait/strategy/ShellStrategy.java | 4 +- .../wait/strategy/WaitAllStrategy.java | 2 +- .../DockerClientProviderStrategy.java | 8 +- .../testcontainers/utility/LazyFuture.java | 4 +- .../utility/RyukResourceReaper.java | 4 +- .../ConstantThroughputRateLimiter.java | 27 ++++ .../utility/ducttape/Preconditions.java | 19 +++ .../utility/ducttape/RateLimiter.java | 59 +++++++++ .../utility/ducttape/RateLimiterBuilder.java | 68 ++++++++++ .../ducttape/RetryCountExceededException.java | 12 ++ .../utility/ducttape/TimeoutException.java | 16 +++ .../utility/ducttape/Timeouts.java | 76 +++++++++++ .../utility/ducttape/Unreliables.java | 124 ++++++++++++++++++ .../ComposeContainerWithServicesTest.java | 2 +- .../containers/ComposeOverridesTest.java | 2 +- ...ockerComposeContainerWithServicesTest.java | 2 +- .../DockerComposeOverridesTest.java | 2 +- .../containers/GenericContainerTest.java | 2 +- ...InternalCommandPortListeningCheckTest.java | 4 +- .../wait/strategy/WaitAllStrategyTest.java | 2 +- .../statement/AbstractStatementTest.java | 2 +- .../junit/ComposeContainerWithBuildTest.java | 2 +- .../DockerComposeContainerWithBuildTest.java | 2 +- .../junit/GenericContainerRuleTest.java | 4 +- .../strategy/AbstractWaitStrategyTest.java | 2 +- .../wait/strategy/HttpWaitStrategyTest.java | 2 +- .../cassandra/CassandraQueryWaitStrategy.java | 4 +- .../wait/CassandraQueryWaitStrategy.java | 4 +- .../couchbase/CouchbaseContainer.java | 2 +- .../localstack/LocalStackContainer.java | 2 +- .../containers/BrowserWebDriverContainer.java | 4 +- .../strategy/YugabyteDBYCQLWaitStrategy.java | 2 +- .../strategy/YugabyteDBYSQLWaitStrategy.java | 2 +- 39 files changed, 447 insertions(+), 49 deletions(-) create mode 100644 core/src/main/java/org/testcontainers/utility/ducttape/ConstantThroughputRateLimiter.java create mode 100644 core/src/main/java/org/testcontainers/utility/ducttape/Preconditions.java create mode 100644 core/src/main/java/org/testcontainers/utility/ducttape/RateLimiter.java create mode 100644 core/src/main/java/org/testcontainers/utility/ducttape/RateLimiterBuilder.java create mode 100644 core/src/main/java/org/testcontainers/utility/ducttape/RetryCountExceededException.java create mode 100644 core/src/main/java/org/testcontainers/utility/ducttape/TimeoutException.java create mode 100644 core/src/main/java/org/testcontainers/utility/ducttape/Timeouts.java create mode 100644 core/src/main/java/org/testcontainers/utility/ducttape/Unreliables.java diff --git a/core/build.gradle b/core/build.gradle index da9ec1f2a44..6e4d9e35a55 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -66,9 +66,6 @@ dependencies { compileOnly 'org.jetbrains:annotations:26.0.2-1' testCompileOnly 'org.jetbrains:annotations:26.0.2-1' api 'org.apache.commons:commons-compress:1.28.0' - api ('org.rnorth.duct-tape:duct-tape:1.0.8') { - exclude(group: 'org.jetbrains', module: 'annotations') - } provided('com.google.cloud.tools:jib-core:0.27.3') { exclude group: 'com.google.guava', module: 'guava' diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 0fe944433ae..1a8d08acf20 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -32,7 +32,6 @@ import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.rnorth.ducttape.unreliables.Unreliables; import org.slf4j.Logger; import org.testcontainers.DockerClientFactory; import org.testcontainers.UnstableAPI; @@ -60,6 +59,7 @@ import org.testcontainers.utility.PathUtils; import org.testcontainers.utility.ResourceReaper; import org.testcontainers.utility.TestcontainersConfiguration; +import org.testcontainers.utility.ducttape.Unreliables; import java.io.File; import java.lang.reflect.InvocationTargetException; diff --git a/core/src/main/java/org/testcontainers/containers/startupcheck/StartupCheckStrategy.java b/core/src/main/java/org/testcontainers/containers/startupcheck/StartupCheckStrategy.java index 50fdcdb8b9b..ab8a9d8fa60 100644 --- a/core/src/main/java/org/testcontainers/containers/startupcheck/StartupCheckStrategy.java +++ b/core/src/main/java/org/testcontainers/containers/startupcheck/StartupCheckStrategy.java @@ -2,10 +2,10 @@ import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.InspectContainerResponse; -import org.rnorth.ducttape.ratelimits.RateLimiter; -import org.rnorth.ducttape.ratelimits.RateLimiterBuilder; -import org.rnorth.ducttape.unreliables.Unreliables; import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.ducttape.RateLimiter; +import org.testcontainers.utility.ducttape.RateLimiterBuilder; +import org.testcontainers.utility.ducttape.Unreliables; import java.time.Duration; import java.util.concurrent.TimeUnit; diff --git a/core/src/main/java/org/testcontainers/containers/wait/strategy/AbstractWaitStrategy.java b/core/src/main/java/org/testcontainers/containers/wait/strategy/AbstractWaitStrategy.java index 4adf28406b6..55917ba3af3 100644 --- a/core/src/main/java/org/testcontainers/containers/wait/strategy/AbstractWaitStrategy.java +++ b/core/src/main/java/org/testcontainers/containers/wait/strategy/AbstractWaitStrategy.java @@ -1,8 +1,8 @@ package org.testcontainers.containers.wait.strategy; import lombok.NonNull; -import org.rnorth.ducttape.ratelimits.RateLimiter; -import org.rnorth.ducttape.ratelimits.RateLimiterBuilder; +import org.testcontainers.utility.ducttape.RateLimiter; +import org.testcontainers.utility.ducttape.RateLimiterBuilder; import java.time.Duration; import java.util.Set; diff --git a/core/src/main/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategy.java b/core/src/main/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategy.java index 60df3cb9efc..af8667d4e3b 100644 --- a/core/src/main/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategy.java +++ b/core/src/main/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategy.java @@ -1,7 +1,7 @@ package org.testcontainers.containers.wait.strategy; -import org.rnorth.ducttape.TimeoutException; -import org.rnorth.ducttape.unreliables.Unreliables; +import org.testcontainers.utility.ducttape.TimeoutException; +import org.testcontainers.utility.ducttape.Unreliables; import org.testcontainers.containers.ContainerLaunchException; import java.util.concurrent.TimeUnit; diff --git a/core/src/main/java/org/testcontainers/containers/wait/strategy/HttpWaitStrategy.java b/core/src/main/java/org/testcontainers/containers/wait/strategy/HttpWaitStrategy.java index 59f6d9077fb..c23a934e66b 100644 --- a/core/src/main/java/org/testcontainers/containers/wait/strategy/HttpWaitStrategy.java +++ b/core/src/main/java/org/testcontainers/containers/wait/strategy/HttpWaitStrategy.java @@ -3,7 +3,7 @@ import com.google.common.base.Strings; import com.google.common.io.BaseEncoding; import lombok.extern.slf4j.Slf4j; -import org.rnorth.ducttape.TimeoutException; +import org.testcontainers.utility.ducttape.TimeoutException; import org.testcontainers.containers.ContainerLaunchException; import java.io.BufferedReader; @@ -34,7 +34,7 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.X509ExtendedTrustManager; -import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess; +import static org.testcontainers.utility.ducttape.Unreliables.retryUntilSuccess; @Slf4j public class HttpWaitStrategy extends AbstractWaitStrategy { diff --git a/core/src/main/java/org/testcontainers/containers/wait/strategy/ShellStrategy.java b/core/src/main/java/org/testcontainers/containers/wait/strategy/ShellStrategy.java index 6c72daa6935..98993d1899a 100644 --- a/core/src/main/java/org/testcontainers/containers/wait/strategy/ShellStrategy.java +++ b/core/src/main/java/org/testcontainers/containers/wait/strategy/ShellStrategy.java @@ -1,7 +1,7 @@ package org.testcontainers.containers.wait.strategy; -import org.rnorth.ducttape.TimeoutException; -import org.rnorth.ducttape.unreliables.Unreliables; +import org.testcontainers.utility.ducttape.TimeoutException; +import org.testcontainers.utility.ducttape.Unreliables; import org.testcontainers.containers.ContainerLaunchException; import java.util.concurrent.TimeUnit; diff --git a/core/src/main/java/org/testcontainers/containers/wait/strategy/WaitAllStrategy.java b/core/src/main/java/org/testcontainers/containers/wait/strategy/WaitAllStrategy.java index 5c7530fc67f..f239c3796dd 100644 --- a/core/src/main/java/org/testcontainers/containers/wait/strategy/WaitAllStrategy.java +++ b/core/src/main/java/org/testcontainers/containers/wait/strategy/WaitAllStrategy.java @@ -1,6 +1,6 @@ package org.testcontainers.containers.wait.strategy; -import org.rnorth.ducttape.timeouts.Timeouts; +import org.testcontainers.utility.ducttape.Timeouts; import java.time.Duration; import java.util.ArrayList; diff --git a/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java b/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java index 7b0aaafc169..b11a9377986 100644 --- a/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java +++ b/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java @@ -19,10 +19,10 @@ import org.apache.commons.lang3.StringUtils; import org.awaitility.Awaitility; import org.jetbrains.annotations.Nullable; -import org.rnorth.ducttape.TimeoutException; -import org.rnorth.ducttape.ratelimits.RateLimiter; -import org.rnorth.ducttape.ratelimits.RateLimiterBuilder; -import org.rnorth.ducttape.unreliables.Unreliables; +import org.testcontainers.utility.ducttape.TimeoutException; +import org.testcontainers.utility.ducttape.RateLimiter; +import org.testcontainers.utility.ducttape.RateLimiterBuilder; +import org.testcontainers.utility.ducttape.Unreliables; import org.testcontainers.DockerClientFactory; import org.testcontainers.UnstableAPI; import org.testcontainers.utility.TestcontainersConfiguration; diff --git a/core/src/main/java/org/testcontainers/utility/LazyFuture.java b/core/src/main/java/org/testcontainers/utility/LazyFuture.java index 3fb2ed704c0..9b7dbc75aed 100644 --- a/core/src/main/java/org/testcontainers/utility/LazyFuture.java +++ b/core/src/main/java/org/testcontainers/utility/LazyFuture.java @@ -2,7 +2,7 @@ import lombok.AccessLevel; import lombok.Getter; -import org.rnorth.ducttape.timeouts.Timeouts; +import org.testcontainers.utility.ducttape.Timeouts; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -45,7 +45,7 @@ public T get() { public T get(long timeout, TimeUnit unit) throws TimeoutException { try { return Timeouts.getWithTimeout((int) timeout, unit, this::get); - } catch (org.rnorth.ducttape.TimeoutException e) { + } catch (org.testcontainers.utility.ducttape.TimeoutException e) { throw new TimeoutException(e.getMessage()); } } diff --git a/core/src/main/java/org/testcontainers/utility/RyukResourceReaper.java b/core/src/main/java/org/testcontainers/utility/RyukResourceReaper.java index 999b1cbbffb..0a699165f11 100644 --- a/core/src/main/java/org/testcontainers/utility/RyukResourceReaper.java +++ b/core/src/main/java/org/testcontainers/utility/RyukResourceReaper.java @@ -3,8 +3,8 @@ import com.github.dockerjava.api.command.CreateContainerCmd; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.rnorth.ducttape.ratelimits.RateLimiter; -import org.rnorth.ducttape.ratelimits.RateLimiterBuilder; +import org.testcontainers.utility.ducttape.RateLimiter; +import org.testcontainers.utility.ducttape.RateLimiterBuilder; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.GenericContainer; diff --git a/core/src/main/java/org/testcontainers/utility/ducttape/ConstantThroughputRateLimiter.java b/core/src/main/java/org/testcontainers/utility/ducttape/ConstantThroughputRateLimiter.java new file mode 100644 index 00000000000..535357121bb --- /dev/null +++ b/core/src/main/java/org/testcontainers/utility/ducttape/ConstantThroughputRateLimiter.java @@ -0,0 +1,27 @@ +package org.testcontainers.utility.ducttape; + +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.TimeUnit; + +/** + * A rate limiter that uses a simple 'run every n millis' strategy to achieve constant throughput. + * This code comes from rnorth/duct-tape + */ +class ConstantThroughputRateLimiter extends RateLimiter { + + private final long timeBetweenInvocations; + + ConstantThroughputRateLimiter(@NotNull Integer rate, @NotNull TimeUnit perTimeUnit) { + this.timeBetweenInvocations = perTimeUnit.toMillis(1) / rate; + } + + @Override + protected long getWaitBeforeNextInvocation() { + + long timeToNextAllowed = (lastInvocation + timeBetweenInvocations) - System.currentTimeMillis(); + + // Clamp wait time to 0< + return Math.max(timeToNextAllowed, 0); + } +} diff --git a/core/src/main/java/org/testcontainers/utility/ducttape/Preconditions.java b/core/src/main/java/org/testcontainers/utility/ducttape/Preconditions.java new file mode 100644 index 00000000000..fd575ba5b66 --- /dev/null +++ b/core/src/main/java/org/testcontainers/utility/ducttape/Preconditions.java @@ -0,0 +1,19 @@ +package org.testcontainers.utility.ducttape; + +/** + * Simple Preconditions check implementation. + * This code comes from rnorth/duct-tape + */ +public class Preconditions { + + /** + * Check that a given condition is true. Will throw an IllegalArgumentException otherwise. + * @param message message to display if the precondition check fails + * @param condition the result of evaluating the condition + */ + public static void check(String message, boolean condition) { + if (!condition) { + throw new IllegalArgumentException("Precondition failed: " + message); + } + } +} diff --git a/core/src/main/java/org/testcontainers/utility/ducttape/RateLimiter.java b/core/src/main/java/org/testcontainers/utility/ducttape/RateLimiter.java new file mode 100644 index 00000000000..adaec917244 --- /dev/null +++ b/core/src/main/java/org/testcontainers/utility/ducttape/RateLimiter.java @@ -0,0 +1,59 @@ +package org.testcontainers.utility.ducttape; + +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.Callable; + +/** + * Base class for rate limiters. Use RateLimiterBuilder to build new instances. + * This code comes from rnorth/duct-tape + */ +public abstract class RateLimiter { + + protected long lastInvocation; + + /** + * Invoke a lambda function, with Thread.sleep() being called to limit the execution rate if needed. + * @param lambda a Runnable lamda function to invoke + */ + public void doWhenReady(@NotNull final Runnable lambda) { + + // Wait before proceeding, if needed + long waitBeforeNextInvocation = getWaitBeforeNextInvocation(); + try { + Thread.sleep(waitBeforeNextInvocation); + } catch (InterruptedException ignored) { } + + try { + lambda.run(); + } finally { + lastInvocation = System.currentTimeMillis(); + } + } + + /** + * + * Invoke a lambda function and get the result, with Thread.sleep() being called to limit the execution rate + * if needed. + * @param lambda a Callable lamda function to invoke + * @param return type of the lamda + * @throws Exception rethrown from lambda + * @return result of the lambda call + */ + public T getWhenReady(@NotNull final Callable lambda) throws Exception { + + // Wait before proceeding, if needed + long waitBeforeNextInvocation = getWaitBeforeNextInvocation(); + try { + Thread.sleep(waitBeforeNextInvocation); + } catch (InterruptedException ignored) { } + + try { + return lambda.call(); + } finally { + lastInvocation = System.currentTimeMillis(); + } + } + + protected abstract long getWaitBeforeNextInvocation(); +} diff --git a/core/src/main/java/org/testcontainers/utility/ducttape/RateLimiterBuilder.java b/core/src/main/java/org/testcontainers/utility/ducttape/RateLimiterBuilder.java new file mode 100644 index 00000000000..aff63bbe870 --- /dev/null +++ b/core/src/main/java/org/testcontainers/utility/ducttape/RateLimiterBuilder.java @@ -0,0 +1,68 @@ +package org.testcontainers.utility.ducttape; + +import java.util.concurrent.TimeUnit; + +import static org.testcontainers.utility.ducttape.Preconditions.check; + +/** + * Builder for rate limiters. + * This code comes from rnorth/duct-tape + */ +public class RateLimiterBuilder { + + private Integer invocations; + private TimeUnit perTimeUnit; + private RateLimiterStrategy strategy; + + private RateLimiterBuilder() { } + + /** + * Obtain a new builder instance. + * @return a new builder + */ + public static RateLimiterBuilder newBuilder() { + return new RateLimiterBuilder(); + } + + /** + * Set the maximum rate that the limiter should allow, expressed as the number of invocations + * allowed in a given time period. + * @param invocations number of invocations + * @param perTimeUnit the time period in which this number of invocations are allowed + * @return the builder + */ + public RateLimiterBuilder withRate(final int invocations, final TimeUnit perTimeUnit) { + this.invocations = invocations; + this.perTimeUnit = perTimeUnit; + return this; + } + + /** + * Configure the rate limiter to use a constant throughput strategy for rate limiting. + * @return the builder + */ + public RateLimiterBuilder withConstantThroughput() { + this.strategy = RateLimiterStrategy.CONSTANT_THROUGHPUT; + return this; + } + + /** + * Build and obtain a configured rate limiter. A rate and rate limiting strategy must have been selected. + * @return the configured rate limiter instance + */ + public RateLimiter build() { + check("A rate must be set", invocations != null); + check("A rate must be set", perTimeUnit != null); + check("A rate limit strategy must be set", strategy != null); + + if (strategy == RateLimiterStrategy.CONSTANT_THROUGHPUT) { + return new ConstantThroughputRateLimiter(invocations, perTimeUnit); + } else { + throw new IllegalStateException(); + } + } + + private enum RateLimiterStrategy { + CONSTANT_THROUGHPUT + } +} diff --git a/core/src/main/java/org/testcontainers/utility/ducttape/RetryCountExceededException.java b/core/src/main/java/org/testcontainers/utility/ducttape/RetryCountExceededException.java new file mode 100644 index 00000000000..fcc29ba2fa1 --- /dev/null +++ b/core/src/main/java/org/testcontainers/utility/ducttape/RetryCountExceededException.java @@ -0,0 +1,12 @@ +package org.testcontainers.utility.ducttape; + +/** + * Indicates repeated failure of an UnreliableSupplier + * This code comes from rnorth/duct-tape + */ +public class RetryCountExceededException extends RuntimeException { + + public RetryCountExceededException(String message, Exception exception) { + super(message, exception); + } +} diff --git a/core/src/main/java/org/testcontainers/utility/ducttape/TimeoutException.java b/core/src/main/java/org/testcontainers/utility/ducttape/TimeoutException.java new file mode 100644 index 00000000000..63e29029b89 --- /dev/null +++ b/core/src/main/java/org/testcontainers/utility/ducttape/TimeoutException.java @@ -0,0 +1,16 @@ +package org.testcontainers.utility.ducttape; + +/** + * Indicates timeout of an UnreliableSupplier + * This code comes from rnorth/duct-tape + */ +public class TimeoutException extends RuntimeException { + + public TimeoutException(String message, Exception exception) { + super(message, exception); + } + + public TimeoutException(Exception e) { + super(e); + } +} diff --git a/core/src/main/java/org/testcontainers/utility/ducttape/Timeouts.java b/core/src/main/java/org/testcontainers/utility/ducttape/Timeouts.java new file mode 100644 index 00000000000..f2aaa89cf8b --- /dev/null +++ b/core/src/main/java/org/testcontainers/utility/ducttape/Timeouts.java @@ -0,0 +1,76 @@ +package org.testcontainers.utility.ducttape; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Utilities to time out on slow running code. + * This code comes from rnorth/duct-tape + */ +public class Timeouts { + + private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool(new ThreadFactory() { + + final AtomicInteger threadCounter = new AtomicInteger(0); + + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, "ducttape-" + threadCounter.getAndIncrement()); + thread.setDaemon(true); + return thread; + } + }); + + /** + * Execute a lambda expression with a timeout. If it completes within the time, the result will be returned. + * If it does not complete within the time, a TimeoutException will be thrown. + * If it throws an exception, a RuntimeException wrapping that exception will be thrown. + * + * @param timeout how long to wait + * @param timeUnit time unit for time interval + * @param lambda supplier lambda expression (may throw checked exceptions) + * @param return type of the lambda + * @return the result of the successful lambda expression call + */ + public static T getWithTimeout(final int timeout, final TimeUnit timeUnit, final Callable lambda) { + + check("timeout must be greater than zero", timeout > 0); + + Future future = EXECUTOR_SERVICE.submit(lambda); + return callFuture(timeout, timeUnit, future); + } + + /** + * Execute a lambda expression with a timeout. If it completes within the time, the result will be returned. + * If it does not complete within the time, a TimeoutException will be thrown. + * If it throws an exception, a RuntimeException wrapping that exception will be thrown. + * + * @param timeout how long to wait + * @param timeUnit time unit for time interval + * @param lambda supplier lambda expression (may throw checked exceptions) + */ + public static void doWithTimeout(final int timeout, final TimeUnit timeUnit, final Runnable lambda) { + + check("timeout must be greater than zero", timeout > 0); + + Future future = EXECUTOR_SERVICE.submit(lambda); + callFuture(timeout, timeUnit, future); + } + + private static T callFuture(final int timeout, final TimeUnit timeUnit, final Future future) { + try { + return future.get(timeout, timeUnit); + } catch (ExecutionException e) { + // The cause of the ExecutionException is the actual exception that was thrown + throw new RuntimeException(e.getCause()); + } catch (java.util.concurrent.TimeoutException | InterruptedException e) { + throw new TimeoutException(e); + } + } + + private static void check(String message, boolean condition) { + if (!condition) { + throw new IllegalArgumentException("Precondition failed: " + message); + } + } +} diff --git a/core/src/main/java/org/testcontainers/utility/ducttape/Unreliables.java b/core/src/main/java/org/testcontainers/utility/ducttape/Unreliables.java new file mode 100644 index 00000000000..c1b9ccafae9 --- /dev/null +++ b/core/src/main/java/org/testcontainers/utility/ducttape/Unreliables.java @@ -0,0 +1,124 @@ +package org.testcontainers.utility.ducttape; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.testcontainers.utility.ducttape.Preconditions.check; + +/** + * Utilities to support automatic retry of things that may fail. + * This code comes from rnorth/duct-tape + */ +public abstract class Unreliables { + + private static final Logger LOGGER = LoggerFactory.getLogger(Unreliables.class); + + /** + * Call a supplier repeatedly until it returns a result. If an exception is thrown, the call + * will be retried repeatedly until the timeout is hit. + * + * @param timeout how long to wait + * @param timeUnit time unit for time interval + * @param lambda supplier lambda expression (may throw checked exceptions) + * @param return type of the supplier + * @return the result of the successful lambda expression call + */ + public static T retryUntilSuccess(final int timeout, final TimeUnit timeUnit, final Callable lambda) { + + check("timeout must be greater than zero", timeout > 0); + + final int[] attempt = {0}; + final Exception[] lastException = {null}; + + final AtomicBoolean doContinue = new AtomicBoolean(true); + try { + return Timeouts.getWithTimeout(timeout, timeUnit, () -> { + while (doContinue.get()) { + try { + return lambda.call(); + } catch (Exception e) { + // Failed + LOGGER.trace("Retrying lambda call on attempt {}", attempt[0]++); + lastException[0] = e; + } + } + return null; + }); + } catch (TimeoutException e) { + if (lastException[0] != null) { + throw new TimeoutException("Timeout waiting for result with exception", lastException[0]); + } else { + throw new TimeoutException(e); + } + } finally { + doContinue.set(false); + } + } + + /** + * Call a supplier repeatedly until it returns a result. If an exception is thrown, the call + * will be retried repeatedly until the retry limit is hit. + * + * @param tryLimit how many times to try calling the supplier + * @param lambda supplier lambda expression (may throw checked exceptions) + * @param return type of the supplier + * @return the result of the successful lambda expression call + */ + public static T retryUntilSuccess(final int tryLimit, final Callable lambda) { + + check("tryLimit must be greater than zero", tryLimit > 0); + + int attempt = 0; + Exception lastException = null; + + while (attempt < tryLimit) { + try { + return lambda.call(); + } catch (Exception e) { + lastException = e; + attempt++; + } + } + + throw new RetryCountExceededException("Retry limit hit with exception", lastException); + } + + /** + * Call a callable repeatedly until it returns true. If an exception is thrown, the call + * will be retried repeatedly until the timeout is hit. + * + * @param timeout how long to wait + * @param timeUnit time unit for time interval + * @param lambda supplier lambda expression + */ + public static void retryUntilTrue(final int timeout, final TimeUnit timeUnit, final Callable lambda) { + retryUntilSuccess(timeout, timeUnit, () -> { + if (!lambda.call()) { + throw new RuntimeException("Not ready yet"); + } else { + return null; + } + }); + } + + /** + * Call a callable repeatedly until it returns true. If an exception is thrown, the call + * will be retried repeatedly until the timeout is hit. + * + * @param tryLimit how many times to try calling the supplier + * @param lambda supplier lambda expression + */ + public static void retryUntilTrue(final int tryLimit, final Callable lambda) { + retryUntilSuccess(tryLimit, () -> { + if (!lambda.call()) { + throw new RuntimeException("Not ready yet"); + } else { + return null; + } + }); + } +} diff --git a/core/src/test/java/org/testcontainers/containers/ComposeContainerWithServicesTest.java b/core/src/test/java/org/testcontainers/containers/ComposeContainerWithServicesTest.java index 0e921316bf3..a67a1ea5e16 100644 --- a/core/src/test/java/org/testcontainers/containers/ComposeContainerWithServicesTest.java +++ b/core/src/test/java/org/testcontainers/containers/ComposeContainerWithServicesTest.java @@ -1,7 +1,7 @@ package org.testcontainers.containers; import org.junit.jupiter.api.Test; -import org.rnorth.ducttape.TimeoutException; +import org.testcontainers.utility.ducttape.TimeoutException; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; diff --git a/core/src/test/java/org/testcontainers/containers/ComposeOverridesTest.java b/core/src/test/java/org/testcontainers/containers/ComposeOverridesTest.java index ed948df5bd3..9e5d2d59b70 100644 --- a/core/src/test/java/org/testcontainers/containers/ComposeOverridesTest.java +++ b/core/src/test/java/org/testcontainers/containers/ComposeOverridesTest.java @@ -5,7 +5,7 @@ import org.assertj.core.api.Assumptions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.rnorth.ducttape.unreliables.Unreliables; +import org.testcontainers.utility.ducttape.Unreliables; import org.testcontainers.utility.CommandLine; import org.testcontainers.utility.DockerImageName; diff --git a/core/src/test/java/org/testcontainers/containers/DockerComposeContainerWithServicesTest.java b/core/src/test/java/org/testcontainers/containers/DockerComposeContainerWithServicesTest.java index 52d8d003003..13317f5d81f 100644 --- a/core/src/test/java/org/testcontainers/containers/DockerComposeContainerWithServicesTest.java +++ b/core/src/test/java/org/testcontainers/containers/DockerComposeContainerWithServicesTest.java @@ -1,7 +1,7 @@ package org.testcontainers.containers; import org.junit.jupiter.api.Test; -import org.rnorth.ducttape.TimeoutException; +import org.testcontainers.utility.ducttape.TimeoutException; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; diff --git a/core/src/test/java/org/testcontainers/containers/DockerComposeOverridesTest.java b/core/src/test/java/org/testcontainers/containers/DockerComposeOverridesTest.java index e3bd499cc45..3fa8dc537ea 100644 --- a/core/src/test/java/org/testcontainers/containers/DockerComposeOverridesTest.java +++ b/core/src/test/java/org/testcontainers/containers/DockerComposeOverridesTest.java @@ -4,7 +4,7 @@ import org.assertj.core.api.Assumptions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.rnorth.ducttape.unreliables.Unreliables; +import org.testcontainers.utility.ducttape.Unreliables; import org.testcontainers.utility.CommandLine; import org.testcontainers.utility.DockerImageName; diff --git a/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java b/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java index 37aac1b0af9..e026003429d 100644 --- a/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java +++ b/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java @@ -18,7 +18,7 @@ import org.apache.commons.io.FileUtils; import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.Test; -import org.rnorth.ducttape.unreliables.Unreliables; +import org.testcontainers.utility.ducttape.Unreliables; import org.testcontainers.DockerClientFactory; import org.testcontainers.TestImages; import org.testcontainers.containers.startupcheck.StartupCheckStrategy; diff --git a/core/src/test/java/org/testcontainers/containers/wait/internal/InternalCommandPortListeningCheckTest.java b/core/src/test/java/org/testcontainers/containers/wait/internal/InternalCommandPortListeningCheckTest.java index 936be73fd3e..69f04eeaab5 100644 --- a/core/src/test/java/org/testcontainers/containers/wait/internal/InternalCommandPortListeningCheckTest.java +++ b/core/src/test/java/org/testcontainers/containers/wait/internal/InternalCommandPortListeningCheckTest.java @@ -4,8 +4,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedClass; import org.junit.jupiter.params.provider.MethodSource; -import org.rnorth.ducttape.TimeoutException; -import org.rnorth.ducttape.unreliables.Unreliables; +import org.testcontainers.utility.ducttape.TimeoutException; +import org.testcontainers.utility.ducttape.Unreliables; import org.testcontainers.containers.GenericContainer; import org.testcontainers.images.builder.ImageFromDockerfile; diff --git a/core/src/test/java/org/testcontainers/containers/wait/strategy/WaitAllStrategyTest.java b/core/src/test/java/org/testcontainers/containers/wait/strategy/WaitAllStrategyTest.java index aeebe425776..ac6d20513c2 100644 --- a/core/src/test/java/org/testcontainers/containers/wait/strategy/WaitAllStrategyTest.java +++ b/core/src/test/java/org/testcontainers/containers/wait/strategy/WaitAllStrategyTest.java @@ -5,7 +5,7 @@ import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.rnorth.ducttape.TimeoutException; +import org.testcontainers.utility.ducttape.TimeoutException; import org.testcontainers.containers.GenericContainer; import java.time.Duration; diff --git a/core/src/test/java/org/testcontainers/images/builder/dockerfile/statement/AbstractStatementTest.java b/core/src/test/java/org/testcontainers/images/builder/dockerfile/statement/AbstractStatementTest.java index 28cb0407f25..5f3c4b29b8e 100644 --- a/core/src/test/java/org/testcontainers/images/builder/dockerfile/statement/AbstractStatementTest.java +++ b/core/src/test/java/org/testcontainers/images/builder/dockerfile/statement/AbstractStatementTest.java @@ -4,7 +4,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.junit.jupiter.api.TestInfo; -import org.rnorth.ducttape.Preconditions; +import org.testcontainers.utility.ducttape.Preconditions; import java.io.InputStream; import java.util.Arrays; diff --git a/core/src/test/java/org/testcontainers/junit/ComposeContainerWithBuildTest.java b/core/src/test/java/org/testcontainers/junit/ComposeContainerWithBuildTest.java index 714767914a0..09e26442978 100644 --- a/core/src/test/java/org/testcontainers/junit/ComposeContainerWithBuildTest.java +++ b/core/src/test/java/org/testcontainers/junit/ComposeContainerWithBuildTest.java @@ -4,7 +4,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.rnorth.ducttape.unreliables.Unreliables; +import org.testcontainers.utility.ducttape.Unreliables; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.ComposeContainer; import org.testcontainers.utility.DockerImageName; diff --git a/core/src/test/java/org/testcontainers/junit/DockerComposeContainerWithBuildTest.java b/core/src/test/java/org/testcontainers/junit/DockerComposeContainerWithBuildTest.java index f0cbe11cbb9..d7e63ccfa5f 100644 --- a/core/src/test/java/org/testcontainers/junit/DockerComposeContainerWithBuildTest.java +++ b/core/src/test/java/org/testcontainers/junit/DockerComposeContainerWithBuildTest.java @@ -4,7 +4,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.rnorth.ducttape.unreliables.Unreliables; +import org.testcontainers.utility.ducttape.Unreliables; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.utility.DockerImageName; diff --git a/core/src/test/java/org/testcontainers/junit/GenericContainerRuleTest.java b/core/src/test/java/org/testcontainers/junit/GenericContainerRuleTest.java index 5719f3e0268..11456a72bc3 100644 --- a/core/src/test/java/org/testcontainers/junit/GenericContainerRuleTest.java +++ b/core/src/test/java/org/testcontainers/junit/GenericContainerRuleTest.java @@ -17,8 +17,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.rnorth.ducttape.RetryCountExceededException; -import org.rnorth.ducttape.unreliables.Unreliables; +import org.testcontainers.utility.ducttape.RetryCountExceededException; +import org.testcontainers.utility.ducttape.Unreliables; import org.testcontainers.TestImages; import org.testcontainers.containers.BindMode; import org.testcontainers.containers.Container; diff --git a/core/src/test/java/org/testcontainers/junit/wait/strategy/AbstractWaitStrategyTest.java b/core/src/test/java/org/testcontainers/junit/wait/strategy/AbstractWaitStrategyTest.java index fa97bbc25c3..a3e65e59098 100644 --- a/core/src/test/java/org/testcontainers/junit/wait/strategy/AbstractWaitStrategyTest.java +++ b/core/src/test/java/org/testcontainers/junit/wait/strategy/AbstractWaitStrategyTest.java @@ -2,7 +2,7 @@ import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; -import org.rnorth.ducttape.RetryCountExceededException; +import org.testcontainers.utility.ducttape.RetryCountExceededException; import org.testcontainers.TestImages; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.GenericContainer; diff --git a/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java b/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java index 89a12ec3aea..ca9a6ce7d7e 100644 --- a/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java +++ b/core/src/test/java/org/testcontainers/junit/wait/strategy/HttpWaitStrategyTest.java @@ -3,7 +3,7 @@ import org.assertj.core.api.Assertions; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; -import org.rnorth.ducttape.RetryCountExceededException; +import org.testcontainers.utility.ducttape.RetryCountExceededException; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.images.builder.ImageFromDockerfile; diff --git a/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraQueryWaitStrategy.java b/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraQueryWaitStrategy.java index 19fdcd7f9e1..a0eee62bd36 100644 --- a/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraQueryWaitStrategy.java +++ b/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraQueryWaitStrategy.java @@ -2,14 +2,14 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.rnorth.ducttape.TimeoutException; +import org.testcontainers.utility.ducttape.TimeoutException; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; import org.testcontainers.delegate.DatabaseDelegate; import java.util.concurrent.TimeUnit; -import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess; +import static org.testcontainers.utility.ducttape.Unreliables.retryUntilSuccess; /** * Waits until Cassandra returns its version diff --git a/modules/cassandra/src/main/java/org/testcontainers/containers/wait/CassandraQueryWaitStrategy.java b/modules/cassandra/src/main/java/org/testcontainers/containers/wait/CassandraQueryWaitStrategy.java index 9694711de6e..53220a817b9 100644 --- a/modules/cassandra/src/main/java/org/testcontainers/containers/wait/CassandraQueryWaitStrategy.java +++ b/modules/cassandra/src/main/java/org/testcontainers/containers/wait/CassandraQueryWaitStrategy.java @@ -1,6 +1,6 @@ package org.testcontainers.containers.wait; -import org.rnorth.ducttape.TimeoutException; +import org.testcontainers.utility.ducttape.TimeoutException; import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.delegate.CassandraDatabaseDelegate; import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; @@ -8,7 +8,7 @@ import java.util.concurrent.TimeUnit; -import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess; +import static org.testcontainers.utility.ducttape.Unreliables.retryUntilSuccess; /** * Waits until Cassandra returns its version diff --git a/modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java b/modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java index 14997d253af..df069e15a50 100644 --- a/modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java +++ b/modules/couchbase/src/main/java/org/testcontainers/couchbase/CouchbaseContainer.java @@ -27,7 +27,7 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; -import org.rnorth.ducttape.unreliables.Unreliables; +import org.testcontainers.utility.ducttape.Unreliables; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; import org.testcontainers.containers.wait.strategy.WaitAllStrategy; diff --git a/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java b/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java index ce50313d429..c8261a7f21d 100644 --- a/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java +++ b/modules/localstack/src/main/java/org/testcontainers/containers/localstack/LocalStackContainer.java @@ -5,13 +5,13 @@ import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; import lombok.extern.slf4j.Slf4j; -import org.rnorth.ducttape.Preconditions; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.ducttape.Preconditions; import java.net.InetAddress; import java.net.URI; diff --git a/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java b/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java index 53ee8ba5577..8c120c58d3a 100644 --- a/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java +++ b/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java @@ -13,8 +13,8 @@ import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.RemoteWebDriver; -import org.rnorth.ducttape.timeouts.Timeouts; -import org.rnorth.ducttape.unreliables.Unreliables; +import org.testcontainers.utility.ducttape.Timeouts; +import org.testcontainers.utility.ducttape.Unreliables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat; diff --git a/modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteDBYCQLWaitStrategy.java b/modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteDBYCQLWaitStrategy.java index 9b9d24a4c71..4f0993481cf 100644 --- a/modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteDBYCQLWaitStrategy.java +++ b/modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteDBYCQLWaitStrategy.java @@ -10,7 +10,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess; +import static org.testcontainers.utility.ducttape.Unreliables.retryUntilSuccess; /** * Custom wait strategy for YCQL API. diff --git a/modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteDBYSQLWaitStrategy.java b/modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteDBYSQLWaitStrategy.java index 4cb5349dc5b..1591c9b80d9 100644 --- a/modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteDBYSQLWaitStrategy.java +++ b/modules/yugabytedb/src/main/java/org/testcontainers/containers/strategy/YugabyteDBYSQLWaitStrategy.java @@ -11,7 +11,7 @@ import java.sql.Statement; import java.util.concurrent.TimeUnit; -import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess; +import static org.testcontainers.utility.ducttape.Unreliables.retryUntilSuccess; /** * Custom wait strategy for YSQL API. From 26349a3b17c138cef66649fad3d4c957885a154d Mon Sep 17 00:00:00 2001 From: David Pilato Date: Tue, 17 Sep 2024 00:53:41 +0200 Subject: [PATCH 2/4] [Bug]: duct-tape:1.0.8 contains a Thread Leak 2nd step: call shutdown() when stopping the container Related to #9227. --- .../java/org/testcontainers/containers/GenericContainer.java | 4 ++++ .../java/org/testcontainers/utility/ducttape/Timeouts.java | 4 ++++ .../testcontainers/cassandra/CassandraQueryWaitStrategy.java | 4 ++-- .../containers/wait/CassandraQueryWaitStrategy.java | 4 ++-- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 1a8d08acf20..8257a2cc9da 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -59,6 +59,7 @@ import org.testcontainers.utility.PathUtils; import org.testcontainers.utility.ResourceReaper; import org.testcontainers.utility.TestcontainersConfiguration; +import org.testcontainers.utility.ducttape.Timeouts; import org.testcontainers.utility.ducttape.Unreliables; import java.io.File; @@ -655,6 +656,9 @@ public void stop() { containerId = null; containerInfo = null; } + + // If the Timeouts class was used, it created a Thread we need to close + Timeouts.shutdown(); } /** 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 f2aaa89cf8b..4433886b4f9 100644 --- a/core/src/main/java/org/testcontainers/utility/ducttape/Timeouts.java +++ b/core/src/main/java/org/testcontainers/utility/ducttape/Timeouts.java @@ -21,6 +21,10 @@ public Thread newThread(Runnable r) { } }); + public static void shutdown() { + EXECUTOR_SERVICE.shutdown(); + } + /** * Execute a lambda expression with a timeout. If it completes within the time, the result will be returned. * If it does not complete within the time, a TimeoutException will be thrown. diff --git a/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraQueryWaitStrategy.java b/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraQueryWaitStrategy.java index a0eee62bd36..8d3e8f774ab 100644 --- a/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraQueryWaitStrategy.java +++ b/modules/cassandra/src/main/java/org/testcontainers/cassandra/CassandraQueryWaitStrategy.java @@ -6,10 +6,10 @@ import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; import org.testcontainers.delegate.DatabaseDelegate; +import org.testcontainers.utility.ducttape.Unreliables; import java.util.concurrent.TimeUnit; -import static org.testcontainers.utility.ducttape.Unreliables.retryUntilSuccess; /** * Waits until Cassandra returns its version @@ -25,7 +25,7 @@ public class CassandraQueryWaitStrategy extends AbstractWaitStrategy { protected void waitUntilReady() { // execute select version query until success or timeout try { - retryUntilSuccess( + Unreliables.retryUntilSuccess( (int) startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> { diff --git a/modules/cassandra/src/main/java/org/testcontainers/containers/wait/CassandraQueryWaitStrategy.java b/modules/cassandra/src/main/java/org/testcontainers/containers/wait/CassandraQueryWaitStrategy.java index 53220a817b9..72c682b7e83 100644 --- a/modules/cassandra/src/main/java/org/testcontainers/containers/wait/CassandraQueryWaitStrategy.java +++ b/modules/cassandra/src/main/java/org/testcontainers/containers/wait/CassandraQueryWaitStrategy.java @@ -5,10 +5,10 @@ import org.testcontainers.containers.delegate.CassandraDatabaseDelegate; import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; import org.testcontainers.delegate.DatabaseDelegate; +import org.testcontainers.utility.ducttape.Unreliables; import java.util.concurrent.TimeUnit; -import static org.testcontainers.utility.ducttape.Unreliables.retryUntilSuccess; /** * Waits until Cassandra returns its version @@ -26,7 +26,7 @@ public class CassandraQueryWaitStrategy extends AbstractWaitStrategy { protected void waitUntilReady() { // execute select version query until success or timeout try { - retryUntilSuccess( + Unreliables.retryUntilSuccess( (int) startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> { From fea72409c6bfa97ac751e6bad49ac8a4a83a30be Mon Sep 17 00:00:00 2001 From: Richard North Date: Fri, 6 Mar 2026 09:31:42 +0000 Subject: [PATCH 3/4] Add POC test exposing bug in Timeouts.shutdown() implementation Demonstrates that calling Timeouts.shutdown() (as GenericContainer.stop() does in this PR) permanently kills the static shared ExecutorService, causing all subsequent Timeouts usage to fail with RejectedExecutionException. This breaks any test suite that stops one container and then starts another. Co-Authored-By: Claude Opus 4.6 --- .../utility/TimeoutsShutdownTest.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 core/src/test/java/org/testcontainers/utility/TimeoutsShutdownTest.java 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..e3da6881cb1 --- /dev/null +++ b/core/src/test/java/org/testcontainers/utility/TimeoutsShutdownTest.java @@ -0,0 +1,35 @@ +package org.testcontainers.utility; + +import org.junit.jupiter.api.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. + */ +class TimeoutsShutdownTest { + + @Test + 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"); + } +} From abb9110336220a91c99fd4e6b1954b133f995efa Mon Sep 17 00:00:00 2001 From: Richard North Date: Fri, 6 Mar 2026 09:45:18 +0000 Subject: [PATCH 4/4] Make Timeouts executor lazily initialized and re-creatable Replace the static final ExecutorService with a lazily-created one so that shutdown() doesn't permanently break subsequent container operations. The executor is re-created transparently on next use. Co-Authored-By: Claude Opus 4.6 --- .../utility/ducttape/Timeouts.java | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) 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); }