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..8257a2cc9da 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,8 @@ 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; import java.lang.reflect.InvocationTargetException; @@ -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/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..4a70340be2f --- /dev/null +++ b/core/src/main/java/org/testcontainers/utility/ducttape/Timeouts.java @@ -0,0 +1,88 @@ +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 AtomicInteger THREAD_COUNTER = new AtomicInteger(0); + + private static final ThreadFactory THREAD_FACTORY = r -> { + Thread thread = new Thread(r, "ducttape-" + THREAD_COUNTER.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 synchronized void shutdown() { + if (executorService != null) { + executorService.shutdown(); + executorService = null; + } + } + + /** + * 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 = getExecutorService().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 = getExecutorService().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/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"); + } +} 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..8d3e8f774ab 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 org.testcontainers.utility.ducttape.Unreliables; import java.util.concurrent.TimeUnit; -import static org.rnorth.ducttape.unreliables.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 9694711de6e..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 @@ -1,14 +1,14 @@ 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; import org.testcontainers.delegate.DatabaseDelegate; +import org.testcontainers.utility.ducttape.Unreliables; import java.util.concurrent.TimeUnit; -import static org.rnorth.ducttape.unreliables.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, () -> { 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.