From 1adf4887f004d3d52e36588c3e36a23169f7933e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Campanero=20Ortiz?= Date: Thu, 18 May 2023 00:29:21 +0200 Subject: [PATCH 01/15] #3829 #6675 Pull retry policy for pulling remote images. --- .../images/RemoteDockerImage.java | 24 +++++----- .../images/retry/DefaultPullRetryPolicy.java | 37 +++++++++++++++ .../images/retry/FailFastPullRetryPolicy.java | 19 ++++++++ .../images/retry/ImagePullRetryPolicy.java | 8 ++++ .../retry/LimitedDurationPullRetryPolicy.java | 44 +++++++++++++++++ .../retry/NoOfAttemptsPullRetryPolicy.java | 31 ++++++++++++ .../images/retry/PullRetryPolicy.java | 47 +++++++++++++++++++ 7 files changed, 198 insertions(+), 12 deletions(-) create mode 100644 core/src/main/java/org/testcontainers/images/retry/DefaultPullRetryPolicy.java create mode 100644 core/src/main/java/org/testcontainers/images/retry/FailFastPullRetryPolicy.java create mode 100644 core/src/main/java/org/testcontainers/images/retry/ImagePullRetryPolicy.java create mode 100644 core/src/main/java/org/testcontainers/images/retry/LimitedDurationPullRetryPolicy.java create mode 100644 core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java create mode 100644 core/src/main/java/org/testcontainers/images/retry/PullRetryPolicy.java diff --git a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java index 7dbd7adec0b..081048c7e2f 100644 --- a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java +++ b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java @@ -14,6 +14,8 @@ import org.slf4j.Logger; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.ContainerFetchException; +import org.testcontainers.images.retry.ImagePullRetryPolicy; +import org.testcontainers.images.retry.PullRetryPolicy; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.DockerLoggerFactory; import org.testcontainers.utility.ImageNameSubstitutor; @@ -29,14 +31,15 @@ @AllArgsConstructor(access = AccessLevel.PACKAGE) public class RemoteDockerImage extends LazyFuture { - private static final Duration PULL_RETRY_TIME_LIMIT = Duration.ofMinutes(2); - @ToString.Exclude private Future imageNameFuture; @With private ImagePullPolicy imagePullPolicy = PullPolicy.defaultPolicy(); + @With + private ImagePullRetryPolicy imagePullRetryPolicy = PullRetryPolicy.defaultRetryPolicy(); + @With private ImageNameSubstitutor imageNameSubstitutor = ImageNameSubstitutor.instance(); @@ -78,9 +81,10 @@ protected final String resolve() { ); Exception lastFailure = null; - final Instant lastRetryAllowed = Instant.now().plus(PULL_RETRY_TIME_LIMIT); + boolean pull = true; + imagePullRetryPolicy.pullStarted(); - while (Instant.now().isBefore(lastRetryAllowed)) { + do { try { PullImageCmd pullImageCmd = dockerClient .pullImageCmd(imageName.getUnversionedPart()) @@ -99,16 +103,12 @@ protected final String resolve() { LocalImagesCache.INSTANCE.refreshCache(imageName); return imageName.asCanonicalNameString(); - } catch (InterruptedException | InternalServerErrorException e) { - // these classes of exception often relate to timeout/connection errors so should be retried + } catch (Exception e) { lastFailure = e; - logger.warn( - "Retrying pull for image: {} ({}s remaining)", - imageName, - Duration.between(Instant.now(), lastRetryAllowed).getSeconds() - ); + pull = imagePullRetryPolicy.shouldRetry(imageName, e); } - } + } while (pull); + logger.error( "Failed to pull image: {}. Please check output of `docker pull {}`", imageName, diff --git a/core/src/main/java/org/testcontainers/images/retry/DefaultPullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/DefaultPullRetryPolicy.java new file mode 100644 index 00000000000..9e20d73aaea --- /dev/null +++ b/core/src/main/java/org/testcontainers/images/retry/DefaultPullRetryPolicy.java @@ -0,0 +1,37 @@ +package org.testcontainers.images.retry; + +import com.github.dockerjava.api.exception.InternalServerErrorException; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.testcontainers.utility.DockerImageName; + +import java.time.Duration; + +/** + * Default pull retry policy. + * + * Will retry on InterruptedException and InternalServerErrorException + * exceptions for a time limit of two minutes. + */ +@Slf4j +@ToString +public class DefaultPullRetryPolicy extends LimitedDurationPullRetryPolicy { + private static final Duration PULL_RETRY_TIME_LIMIT = Duration.ofMinutes(2); + + public DefaultPullRetryPolicy() { + super(PULL_RETRY_TIME_LIMIT); + } + + @Override + public boolean shouldRetry(DockerImageName imageName, Throwable error) { + if (!mayRetry(error)) { + return false; + } + + return super.shouldRetry(imageName, error); + } + + private boolean mayRetry(Throwable error) { + return error instanceof InterruptedException || error instanceof InternalServerErrorException; + } +} diff --git a/core/src/main/java/org/testcontainers/images/retry/FailFastPullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/FailFastPullRetryPolicy.java new file mode 100644 index 00000000000..9a8b6feda84 --- /dev/null +++ b/core/src/main/java/org/testcontainers/images/retry/FailFastPullRetryPolicy.java @@ -0,0 +1,19 @@ +package org.testcontainers.images.retry; + +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.testcontainers.utility.DockerImageName; + +/** + * Fail-fast, i.e. not retry, pull policy + */ +@Slf4j +@ToString +public class FailFastPullRetryPolicy implements ImagePullRetryPolicy { + + @Override + public boolean shouldRetry(DockerImageName imageName, Throwable error) { + return false; + } + +} diff --git a/core/src/main/java/org/testcontainers/images/retry/ImagePullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/ImagePullRetryPolicy.java new file mode 100644 index 00000000000..f53bb43aae5 --- /dev/null +++ b/core/src/main/java/org/testcontainers/images/retry/ImagePullRetryPolicy.java @@ -0,0 +1,8 @@ +package org.testcontainers.images.retry; + +import org.testcontainers.utility.DockerImageName; + +public interface ImagePullRetryPolicy { + default void pullStarted() {} + boolean shouldRetry(DockerImageName imageName, Throwable error); +} diff --git a/core/src/main/java/org/testcontainers/images/retry/LimitedDurationPullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/LimitedDurationPullRetryPolicy.java new file mode 100644 index 00000000000..325adcf55e0 --- /dev/null +++ b/core/src/main/java/org/testcontainers/images/retry/LimitedDurationPullRetryPolicy.java @@ -0,0 +1,44 @@ +package org.testcontainers.images.retry; + +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.testcontainers.utility.DockerImageName; + +import java.time.Duration; +import java.time.Instant; + +/** + * An ImagePullRetryPolicy which will retry a failed image pull if the time elapsed since the + * pull started is less than or equal to the configured {@link #maxAllowedDuration}. + * + */ +@Slf4j +@RequiredArgsConstructor +@ToString +public class LimitedDurationPullRetryPolicy implements ImagePullRetryPolicy { + + @NonNull + @Getter + Duration maxAllowedDuration; + + Instant lastRetryAllowed; + + @Override + public void pullStarted() { + this.lastRetryAllowed = Instant.now().plus(maxAllowedDuration); + } + + @Override + public boolean shouldRetry(DockerImageName imageName, Throwable error) { + log.warn( + "Retrying pull for image: {} ({}s remaining)", + imageName, + Duration.between(Instant.now(), lastRetryAllowed).getSeconds() + ); + + return Instant.now().isBefore(lastRetryAllowed); + } +} diff --git a/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java new file mode 100644 index 00000000000..3791bf5debc --- /dev/null +++ b/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java @@ -0,0 +1,31 @@ +package org.testcontainers.images.retry; + +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.testcontainers.utility.DockerImageName; + +/** + * An ImagePullRetryPolicy which will retry a failed image pull if the number of attempts + * is less than or equal to the configured {@link #maxAllowedNoOfAttempts}. + * + */ +@Slf4j +@RequiredArgsConstructor +@ToString +public class NoOfAttemptsPullRetryPolicy implements ImagePullRetryPolicy { + + @NonNull + @Getter + private final int maxAllowedNoOfAttempts; + + private int currentNoOfAttempts = 0; + + @Override + public boolean shouldRetry(DockerImageName imageName, Throwable error) { + return ++currentNoOfAttempts > maxAllowedNoOfAttempts; + } + +} diff --git a/core/src/main/java/org/testcontainers/images/retry/PullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/PullRetryPolicy.java new file mode 100644 index 00000000000..f7870c6ab35 --- /dev/null +++ b/core/src/main/java/org/testcontainers/images/retry/PullRetryPolicy.java @@ -0,0 +1,47 @@ +package org.testcontainers.images.retry; + +import lombok.experimental.UtilityClass; + +import java.time.Duration; + +/** + * Convenience class with logic for building common {@link ImagePullRetryPolicy} instances. + * + */ +@UtilityClass +public class PullRetryPolicy { + + /** + * Convenience method for returning the {@link FailFastPullRetryPolicy} failFast image pull retry policy + * @return {@link ImagePullRetryPolicy} + */ + public static ImagePullRetryPolicy failFast() { + return new FailFastPullRetryPolicy(); + } + + /** + * Convenience method for returning the {@link NoOfAttemptsPullRetryPolicy} number of attempts based image pull + * retry policy. + * @return {@link ImagePullRetryPolicy} + */ + public static ImagePullRetryPolicy noOfAttempts(int allowedNoOfAttempts) { + return new NoOfAttemptsPullRetryPolicy(allowedNoOfAttempts); + } + + /** + * Convenience method for returning the {@link LimitedDurationPullRetryPolicy} duration image pull retry policy + * @return {@link ImagePullRetryPolicy} + */ + public static ImagePullRetryPolicy limitedDuration(Duration maxAllowedDuration) { + return new LimitedDurationPullRetryPolicy(maxAllowedDuration); + } + + /** + * Convenience method for returning the {@link DefaultPullRetryPolicy} default image pull retry policy. + * @return {@link ImagePullRetryPolicy} + */ + public static ImagePullRetryPolicy defaultRetryPolicy() { + return new DefaultPullRetryPolicy(); + } + +} From 2384f67a3231aa3220afc3ce4941007ff6a28c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Campanero=20Ortiz?= Date: Thu, 18 May 2023 00:51:57 +0200 Subject: [PATCH 02/15] Check style fixes. --- .../main/java/org/testcontainers/images/RemoteDockerImage.java | 3 --- .../testcontainers/images/retry/DefaultPullRetryPolicy.java | 1 + .../testcontainers/images/retry/FailFastPullRetryPolicy.java | 1 - .../org/testcontainers/images/retry/ImagePullRetryPolicy.java | 1 + .../images/retry/NoOfAttemptsPullRetryPolicy.java | 1 - .../java/org/testcontainers/images/retry/PullRetryPolicy.java | 1 - 6 files changed, 2 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java index 081048c7e2f..a08dab64f5e 100644 --- a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java +++ b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java @@ -3,7 +3,6 @@ import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.PullImageCmd; import com.github.dockerjava.api.exception.DockerClientException; -import com.github.dockerjava.api.exception.InternalServerErrorException; import com.google.common.util.concurrent.Futures; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -21,8 +20,6 @@ import org.testcontainers.utility.ImageNameSubstitutor; import org.testcontainers.utility.LazyFuture; -import java.time.Duration; -import java.time.Instant; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; diff --git a/core/src/main/java/org/testcontainers/images/retry/DefaultPullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/DefaultPullRetryPolicy.java index 9e20d73aaea..97fa5760937 100644 --- a/core/src/main/java/org/testcontainers/images/retry/DefaultPullRetryPolicy.java +++ b/core/src/main/java/org/testcontainers/images/retry/DefaultPullRetryPolicy.java @@ -16,6 +16,7 @@ @Slf4j @ToString public class DefaultPullRetryPolicy extends LimitedDurationPullRetryPolicy { + private static final Duration PULL_RETRY_TIME_LIMIT = Duration.ofMinutes(2); public DefaultPullRetryPolicy() { diff --git a/core/src/main/java/org/testcontainers/images/retry/FailFastPullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/FailFastPullRetryPolicy.java index 9a8b6feda84..0d852220286 100644 --- a/core/src/main/java/org/testcontainers/images/retry/FailFastPullRetryPolicy.java +++ b/core/src/main/java/org/testcontainers/images/retry/FailFastPullRetryPolicy.java @@ -15,5 +15,4 @@ public class FailFastPullRetryPolicy implements ImagePullRetryPolicy { public boolean shouldRetry(DockerImageName imageName, Throwable error) { return false; } - } diff --git a/core/src/main/java/org/testcontainers/images/retry/ImagePullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/ImagePullRetryPolicy.java index f53bb43aae5..e45783df2ea 100644 --- a/core/src/main/java/org/testcontainers/images/retry/ImagePullRetryPolicy.java +++ b/core/src/main/java/org/testcontainers/images/retry/ImagePullRetryPolicy.java @@ -4,5 +4,6 @@ public interface ImagePullRetryPolicy { default void pullStarted() {} + boolean shouldRetry(DockerImageName imageName, Throwable error); } diff --git a/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java index 3791bf5debc..93b21c4b9d7 100644 --- a/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java +++ b/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java @@ -27,5 +27,4 @@ public class NoOfAttemptsPullRetryPolicy implements ImagePullRetryPolicy { public boolean shouldRetry(DockerImageName imageName, Throwable error) { return ++currentNoOfAttempts > maxAllowedNoOfAttempts; } - } diff --git a/core/src/main/java/org/testcontainers/images/retry/PullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/PullRetryPolicy.java index f7870c6ab35..134c661d798 100644 --- a/core/src/main/java/org/testcontainers/images/retry/PullRetryPolicy.java +++ b/core/src/main/java/org/testcontainers/images/retry/PullRetryPolicy.java @@ -43,5 +43,4 @@ public static ImagePullRetryPolicy limitedDuration(Duration maxAllowedDuration) public static ImagePullRetryPolicy defaultRetryPolicy() { return new DefaultPullRetryPolicy(); } - } From 355e9f72c4ea6387ce7b3ae59f5e7e17eb3311ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Campanero=20Ortiz?= Date: Thu, 18 May 2023 00:57:00 +0200 Subject: [PATCH 03/15] Retry indication log message minor fix. --- .../retry/LimitedDurationPullRetryPolicy.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/testcontainers/images/retry/LimitedDurationPullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/LimitedDurationPullRetryPolicy.java index 325adcf55e0..d28859f4129 100644 --- a/core/src/main/java/org/testcontainers/images/retry/LimitedDurationPullRetryPolicy.java +++ b/core/src/main/java/org/testcontainers/images/retry/LimitedDurationPullRetryPolicy.java @@ -33,12 +33,16 @@ public void pullStarted() { @Override public boolean shouldRetry(DockerImageName imageName, Throwable error) { - log.warn( - "Retrying pull for image: {} ({}s remaining)", - imageName, - Duration.between(Instant.now(), lastRetryAllowed).getSeconds() - ); + if (Instant.now().isBefore(lastRetryAllowed)) { + log.warn( + "Retrying pull for image: {} ({}s remaining)", + imageName, + Duration.between(Instant.now(), lastRetryAllowed).getSeconds() + ); - return Instant.now().isBefore(lastRetryAllowed); + return true; + } + + return false; } } From 17e34143faff541adf56082187c397147890fef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Campanero=20Ortiz?= Date: Thu, 18 May 2023 01:04:52 +0200 Subject: [PATCH 04/15] Update NoOfAttemptsPullRetryPolicy Fix number of attempts comparison expression. --- .../images/retry/NoOfAttemptsPullRetryPolicy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java index 93b21c4b9d7..f45b2a4a68d 100644 --- a/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java +++ b/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java @@ -25,6 +25,6 @@ public class NoOfAttemptsPullRetryPolicy implements ImagePullRetryPolicy { @Override public boolean shouldRetry(DockerImageName imageName, Throwable error) { - return ++currentNoOfAttempts > maxAllowedNoOfAttempts; + return ++currentNoOfAttempts <= maxAllowedNoOfAttempts; } } From 2da1d5ca09c49d13d4b16ae82fe4b0e541a25646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Campanero=20Ortiz?= Date: Fri, 19 May 2023 00:52:53 +0200 Subject: [PATCH 05/15] Add retry policy setters to container base classes. --- .../main/java/org/testcontainers/containers/Container.java | 7 +++++++ .../org/testcontainers/containers/GenericContainer.java | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/core/src/main/java/org/testcontainers/containers/Container.java b/core/src/main/java/org/testcontainers/containers/Container.java index 7e6f492c284..0faaee136bb 100644 --- a/core/src/main/java/org/testcontainers/containers/Container.java +++ b/core/src/main/java/org/testcontainers/containers/Container.java @@ -11,6 +11,7 @@ import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.images.ImagePullPolicy; import org.testcontainers.images.builder.Transferable; +import org.testcontainers.images.retry.ImagePullRetryPolicy; import org.testcontainers.utility.LogUtils; import org.testcontainers.utility.MountableFile; @@ -289,6 +290,12 @@ default SELF withEnv(String key, Function, String> mapper) { */ SELF withImagePullPolicy(ImagePullPolicy policy); + /** + * Set the image retry on pull error policy of the container + * @return + */ + SELF withImagePullRetryPolicy(ImagePullRetryPolicy policy); + /** * Map a resource (file or directory) on the classpath to a path inside the container. * This will only work if you are running your tests outside a Docker container. diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 959b14414f0..85a6e953f71 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -51,6 +51,7 @@ import org.testcontainers.images.ImagePullPolicy; import org.testcontainers.images.RemoteDockerImage; import org.testcontainers.images.builder.Transferable; +import org.testcontainers.images.retry.ImagePullRetryPolicy; import org.testcontainers.lifecycle.Startable; import org.testcontainers.lifecycle.Startables; import org.testcontainers.lifecycle.TestDescription; @@ -1281,6 +1282,12 @@ public SELF withImagePullPolicy(ImagePullPolicy imagePullPolicy) { return self(); } + @Override + public SELF withImagePullRetryPolicy(ImagePullRetryPolicy policy) { + this.image = this.image.withImagePullRetryPolicy(policy); + return self(); + } + /** * {@inheritDoc} */ From c4bb3c3eb332169fd1426fde52acf5c533acb683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Campanero=20Ortiz?= Date: Sat, 20 May 2023 00:23:30 +0200 Subject: [PATCH 06/15] Add unit test cases for the different retry pull policies. --- .../retry/ImagePullRetryPolicyTest.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 core/src/test/java/org/testcontainers/images/retry/ImagePullRetryPolicyTest.java diff --git a/core/src/test/java/org/testcontainers/images/retry/ImagePullRetryPolicyTest.java b/core/src/test/java/org/testcontainers/images/retry/ImagePullRetryPolicyTest.java new file mode 100644 index 00000000000..46192d3d8c8 --- /dev/null +++ b/core/src/test/java/org/testcontainers/images/retry/ImagePullRetryPolicyTest.java @@ -0,0 +1,72 @@ +package org.testcontainers.images.retry; + +import com.github.dockerjava.api.exception.InternalServerErrorException; +import org.junit.ClassRule; +import org.junit.Test; +import org.testcontainers.DockerRegistryContainer; +import org.testcontainers.utility.DockerImageName; + +import java.time.Duration; +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ImagePullRetryPolicyTest { + + @ClassRule + public static DockerRegistryContainer registry = new DockerRegistryContainer(); + + private final DockerImageName imageName = registry.createImage(); + + @Test + public void shouldNotRetryWhenUsingFailFastPullRetryPolicy() { + ImagePullRetryPolicy policy = PullRetryPolicy.failFast(); + policy.pullStarted(); + assertThat(policy.shouldRetry(imageName, new Exception())).isFalse(); + } + + @Test + public void shouldRetryDuringTheConfiguredAmountOfTimeWhenUsingLimitedDurationPullRetryPolicy() { + Duration maxAllowedDuration = Duration.ofMillis(100); + Instant lastRetryAllowed = Instant.now().plus(maxAllowedDuration); + ImagePullRetryPolicy policy = PullRetryPolicy.limitedDuration(maxAllowedDuration); + policy.pullStarted(); + while (Instant.now().isBefore(lastRetryAllowed)) { + assertThat(policy.shouldRetry(imageName, new Exception())).isTrue(); + } + + assertThat(policy.shouldRetry(imageName, new Exception())).isFalse(); + } + + @Test + public void shouldRetryTheConfiguredNumberOfAttemptsWhenUsingNoOfAttemptsPullRetryPolicy() { + int allowedNoOfAttempts = 4; + ImagePullRetryPolicy policy = PullRetryPolicy.noOfAttempts(allowedNoOfAttempts); + policy.pullStarted(); + while (allowedNoOfAttempts-- > 0) { + assertThat(policy.shouldRetry(imageName, new Exception())).isTrue(); + } + + assertThat(policy.shouldRetry(imageName, new Exception())).isFalse(); + } + + @Test + public void shouldNotRetryWhenUsingDefaultPullRetryPolicyAndExceptionIsNotRetriable() { + ImagePullRetryPolicy policy = PullRetryPolicy.defaultRetryPolicy(); + policy.pullStarted(); + assertThat(policy.shouldRetry(imageName, new Exception())).isFalse(); + } + + @Test + public void shouldRetryWhenUsingDefaultPullRetryPolicyAndExceptionIsRetriableAndTheElapsedTimeIsUnderTheDefaut() { + // I don't see a convenient way to test the default two minutes timeout: I rather + // prefer to not test it + ImagePullRetryPolicy policy = PullRetryPolicy.defaultRetryPolicy(); + policy.pullStarted(); + assertThat(policy.shouldRetry(imageName, new InterruptedException())).isTrue(); + assertThat( + policy.shouldRetry(imageName, + new InternalServerErrorException("The message is not important for the test")) + ).isTrue(); + } +} From 4199b5a6236c3af6d1e537414b60dc5170103995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Campanero=20Ortiz?= Date: Sun, 21 May 2023 00:16:09 +0200 Subject: [PATCH 07/15] Argument sanity checks. --- .../retry/LimitedDurationPullRetryPolicy.java | 22 ++++++++--- .../retry/NoOfAttemptsPullRetryPolicy.java | 16 ++++++-- .../retry/ImagePullRetryPolicyTest.java | 39 ++++++++++++++++--- 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/org/testcontainers/images/retry/LimitedDurationPullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/LimitedDurationPullRetryPolicy.java index d28859f4129..decab925260 100644 --- a/core/src/main/java/org/testcontainers/images/retry/LimitedDurationPullRetryPolicy.java +++ b/core/src/main/java/org/testcontainers/images/retry/LimitedDurationPullRetryPolicy.java @@ -1,8 +1,6 @@ package org.testcontainers.images.retry; import lombok.Getter; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.testcontainers.utility.DockerImageName; @@ -16,16 +14,26 @@ * */ @Slf4j -@RequiredArgsConstructor @ToString public class LimitedDurationPullRetryPolicy implements ImagePullRetryPolicy { - @NonNull @Getter - Duration maxAllowedDuration; + private final Duration maxAllowedDuration; Instant lastRetryAllowed; + public LimitedDurationPullRetryPolicy(Duration maxAllowedDuration) { + if (maxAllowedDuration == null) { + throw new NullPointerException("maxAllowedDuration should not be null"); + } + + if (maxAllowedDuration.isNegative()) { + throw new IllegalArgumentException("maxAllowedDuration should not be negative"); + } + + this.maxAllowedDuration = maxAllowedDuration; + } + @Override public void pullStarted() { this.lastRetryAllowed = Instant.now().plus(maxAllowedDuration); @@ -33,6 +41,10 @@ public void pullStarted() { @Override public boolean shouldRetry(DockerImageName imageName, Throwable error) { + if (lastRetryAllowed == null) { + throw new IllegalStateException("lastRetryAllowed is null. Please, check that pullStarted has been called."); + } + if (Instant.now().isBefore(lastRetryAllowed)) { log.warn( "Retrying pull for image: {} ({}s remaining)", diff --git a/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java index f45b2a4a68d..e35ca8a026f 100644 --- a/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java +++ b/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java @@ -1,8 +1,6 @@ package org.testcontainers.images.retry; import lombok.Getter; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.testcontainers.utility.DockerImageName; @@ -13,16 +11,26 @@ * */ @Slf4j -@RequiredArgsConstructor @ToString public class NoOfAttemptsPullRetryPolicy implements ImagePullRetryPolicy { - @NonNull @Getter private final int maxAllowedNoOfAttempts; private int currentNoOfAttempts = 0; + public NoOfAttemptsPullRetryPolicy(Integer maxAllowedNoOfAttempts) { + if (maxAllowedNoOfAttempts == null) { + throw new NullPointerException("maxAllowedNoOfAttempts should not be null"); + } + + if (maxAllowedNoOfAttempts < 0) { + throw new IllegalArgumentException("maxAllowedNoOfAttempts should not be negative"); + } + + this.maxAllowedNoOfAttempts = maxAllowedNoOfAttempts; + } + @Override public boolean shouldRetry(DockerImageName imageName, Throwable error) { return ++currentNoOfAttempts <= maxAllowedNoOfAttempts; diff --git a/core/src/test/java/org/testcontainers/images/retry/ImagePullRetryPolicyTest.java b/core/src/test/java/org/testcontainers/images/retry/ImagePullRetryPolicyTest.java index 46192d3d8c8..a2c209cf210 100644 --- a/core/src/test/java/org/testcontainers/images/retry/ImagePullRetryPolicyTest.java +++ b/core/src/test/java/org/testcontainers/images/retry/ImagePullRetryPolicyTest.java @@ -2,7 +2,9 @@ import com.github.dockerjava.api.exception.InternalServerErrorException; import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.testcontainers.DockerRegistryContainer; import org.testcontainers.utility.DockerImageName; @@ -18,6 +20,9 @@ public class ImagePullRetryPolicyTest { private final DockerImageName imageName = registry.createImage(); + @Rule + public ExpectedException thrown = ExpectedException.none(); + @Test public void shouldNotRetryWhenUsingFailFastPullRetryPolicy() { ImagePullRetryPolicy policy = PullRetryPolicy.failFast(); @@ -25,6 +30,21 @@ public void shouldNotRetryWhenUsingFailFastPullRetryPolicy() { assertThat(policy.shouldRetry(imageName, new Exception())).isFalse(); } + @Test + public void shouldFailIfTheConfiguredDurationIsNegativeWhenUsingLimitedDurationPullRetryPolicy() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("should not be negative"); + PullRetryPolicy.limitedDuration(Duration.ofMinutes(-1)); + } + + @Test + public void shouldFailIfPullStartedIsNotBeingCalledBeforeShouldRetryWhenUsingLimitedDurationPullRetryPolicy() { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Please, check that pullStarted has been called."); + ImagePullRetryPolicy policy = PullRetryPolicy.limitedDuration(Duration.ofMinutes(1)); + policy.shouldRetry(imageName, new Exception()); + } + @Test public void shouldRetryDuringTheConfiguredAmountOfTimeWhenUsingLimitedDurationPullRetryPolicy() { Duration maxAllowedDuration = Duration.ofMillis(100); @@ -38,12 +58,19 @@ public void shouldRetryDuringTheConfiguredAmountOfTimeWhenUsingLimitedDurationPu assertThat(policy.shouldRetry(imageName, new Exception())).isFalse(); } + @Test + public void shouldFailIfTheConfiguredNumberOfAttemptsIsNegativeWhenUsingNoOfAttemptsPullRetryPolicy() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("should not be negative"); + PullRetryPolicy.noOfAttempts(-1); + } + @Test public void shouldRetryTheConfiguredNumberOfAttemptsWhenUsingNoOfAttemptsPullRetryPolicy() { - int allowedNoOfAttempts = 4; - ImagePullRetryPolicy policy = PullRetryPolicy.noOfAttempts(allowedNoOfAttempts); + int anyNumberOfOfAttemptsGreaterThanOrEqualToZero = 4; + ImagePullRetryPolicy policy = PullRetryPolicy.noOfAttempts(anyNumberOfOfAttemptsGreaterThanOrEqualToZero); policy.pullStarted(); - while (allowedNoOfAttempts-- > 0) { + while (anyNumberOfOfAttemptsGreaterThanOrEqualToZero-- > 0) { assertThat(policy.shouldRetry(imageName, new Exception())).isTrue(); } @@ -65,8 +92,8 @@ public void shouldRetryWhenUsingDefaultPullRetryPolicyAndExceptionIsRetriableAnd policy.pullStarted(); assertThat(policy.shouldRetry(imageName, new InterruptedException())).isTrue(); assertThat( - policy.shouldRetry(imageName, - new InternalServerErrorException("The message is not important for the test")) - ).isTrue(); + policy.shouldRetry(imageName, new InternalServerErrorException("The message is not important for the test")) + ) + .isTrue(); } } From 8fca4e84919e2a50365ee048c837b0d16a3e9991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Campanero=20Ortiz?= Date: Thu, 12 Oct 2023 23:50:45 +0200 Subject: [PATCH 08/15] Fix import errors when merging. --- .../main/java/org/testcontainers/images/RemoteDockerImage.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java index bafe5f9a06b..7d7c54f1790 100644 --- a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java +++ b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java @@ -20,6 +20,8 @@ import org.testcontainers.utility.ImageNameSubstitutor; import org.testcontainers.utility.LazyFuture; +import java.time.Duration; +import java.time.Instant; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; From 99459c77cc863685f4f52eb539023bd6758be1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Campanero=20Ortiz?= Date: Sun, 1 Mar 2026 00:07:11 +0100 Subject: [PATCH 09/15] feat: update default retry policy to use the value in config In a similar way as defined in #9417. --- .../testcontainers/images/retry/DefaultPullRetryPolicy.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/testcontainers/images/retry/DefaultPullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/DefaultPullRetryPolicy.java index 97fa5760937..bd4cdd4010b 100644 --- a/core/src/main/java/org/testcontainers/images/retry/DefaultPullRetryPolicy.java +++ b/core/src/main/java/org/testcontainers/images/retry/DefaultPullRetryPolicy.java @@ -4,6 +4,7 @@ import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.TestcontainersConfiguration; import java.time.Duration; @@ -17,7 +18,9 @@ @ToString public class DefaultPullRetryPolicy extends LimitedDurationPullRetryPolicy { - private static final Duration PULL_RETRY_TIME_LIMIT = Duration.ofMinutes(2); + private static final Duration PULL_RETRY_TIME_LIMIT = Duration.ofSeconds( + TestcontainersConfiguration.getInstance().getImagePullTimeout() + ); public DefaultPullRetryPolicy() { super(PULL_RETRY_TIME_LIMIT); From 3aeaad94b0162d90f7102b6a8ae9553ba10b45cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Campanero=20Ortiz?= Date: Sun, 1 Mar 2026 01:08:37 +0100 Subject: [PATCH 10/15] feat: adapt the original proposal to the current code --- .../images/RemoteDockerImage.java | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java index 9d669e46b07..2c32f8cf440 100644 --- a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java +++ b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java @@ -3,7 +3,6 @@ import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.PullImageCmd; import com.github.dockerjava.api.exception.DockerClientException; -import com.github.dockerjava.api.exception.InternalServerErrorException; import com.github.dockerjava.api.exception.NotFoundException; import com.google.common.util.concurrent.Futures; import lombok.AccessLevel; @@ -18,6 +17,8 @@ import org.slf4j.Logger; import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.ContainerFetchException; +import org.testcontainers.images.retry.ImagePullRetryPolicy; +import org.testcontainers.images.retry.PullRetryPolicy; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.DockerLoggerFactory; import org.testcontainers.utility.ImageNameSubstitutor; @@ -46,6 +47,9 @@ public class RemoteDockerImage extends LazyFuture { @With ImagePullPolicy imagePullPolicy = PullPolicy.defaultPolicy(); + @With + private ImagePullRetryPolicy imagePullRetryPolicy = PullRetryPolicy.defaultRetryPolicy(); + @With private ImageNameSubstitutor imageNameSubstitutor = ImageNameSubstitutor.instance(); @@ -87,7 +91,6 @@ protected final String resolve() { ); final Instant startedAt = Instant.now(); - final Instant lastRetryAllowed = Instant.now().plus(PULL_RETRY_TIME_LIMIT); final AtomicReference lastFailure = new AtomicReference<>(); final PullImageCmd pullImageCmd = dockerClient .pullImageCmd(imageName.getUnversionedPart()) @@ -100,6 +103,8 @@ protected final String resolve() { .iterative(duration -> duration.multipliedBy(2)) .startDuration(Duration.ofMillis(50)); + imagePullRetryPolicy.pullStarted(); + Awaitility .await() .pollInSameThread() @@ -107,7 +112,7 @@ protected final String resolve() { .atMost(PULL_RETRY_TIME_LIMIT) .pollInterval(interval) .until( - tryImagePullCommand(pullImageCmd, logger, dockerImageName, imageName, lastFailure, lastRetryAllowed) + tryImagePullCommand(pullImageCmd, logger, dockerImageName, imageName, lastFailure, imagePullRetryPolicy) ); if (dockerImageName.get() == null) { @@ -135,23 +140,23 @@ private Callable tryImagePullCommand( AtomicReference dockerImageName, DockerImageName imageName, AtomicReference lastFailure, - Instant lastRetryAllowed + ImagePullRetryPolicy imagePullRetryPolicy ) { return () -> { - try { - pullImage(pullImageCmd, logger); - dockerImageName.set(imageName.asCanonicalNameString()); - return true; - } catch (InterruptedException | InternalServerErrorException e) { - // these classes of exception often relate to timeout/connection errors so should be retried - lastFailure.set(e); - logger.warn( - "Retrying pull for image: {} ({}s remaining)", - imageName, - Duration.between(Instant.now(), lastRetryAllowed).getSeconds() - ); - return false; - } + boolean pull; + + do { + try { + pullImage(pullImageCmd, logger); + dockerImageName.set(imageName.asCanonicalNameString()); + return true; + } catch (Exception e) { + lastFailure.set(e); + pull = imagePullRetryPolicy.shouldRetry(imageName, e); + } + } while (pull); + + return false; }; } From 95f87e7c008a0de20a6784640c7e0502cf1c5fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Campanero=20Ortiz?= Date: Sun, 1 Mar 2026 01:08:58 +0100 Subject: [PATCH 11/15] docs: minor javadoc fixes --- .../src/main/java/org/testcontainers/containers/Container.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/testcontainers/containers/Container.java b/core/src/main/java/org/testcontainers/containers/Container.java index b685f364474..f8a0b55bf82 100644 --- a/core/src/main/java/org/testcontainers/containers/Container.java +++ b/core/src/main/java/org/testcontainers/containers/Container.java @@ -300,7 +300,8 @@ default SELF withEnv(String key, Function, String> mapper) { /** * Set the image retry on pull error policy of the container - * @return + * @param policy the image pull retry policy + * @return this */ SELF withImagePullRetryPolicy(ImagePullRetryPolicy policy); From 43c21ebc80751a782ce544db398b3d538e9f9b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Campanero=20Ortiz?= Date: Sun, 1 Mar 2026 13:50:31 +0100 Subject: [PATCH 12/15] feat: mfeat: improve thread safety in component --- .../images/retry/NoOfAttemptsPullRetryPolicy.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java index e35ca8a026f..367e7f47395 100644 --- a/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java +++ b/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java @@ -5,6 +5,8 @@ import lombok.extern.slf4j.Slf4j; import org.testcontainers.utility.DockerImageName; +import java.util.concurrent.atomic.AtomicInteger; + /** * An ImagePullRetryPolicy which will retry a failed image pull if the number of attempts * is less than or equal to the configured {@link #maxAllowedNoOfAttempts}. @@ -17,13 +19,9 @@ public class NoOfAttemptsPullRetryPolicy implements ImagePullRetryPolicy { @Getter private final int maxAllowedNoOfAttempts; - private int currentNoOfAttempts = 0; - - public NoOfAttemptsPullRetryPolicy(Integer maxAllowedNoOfAttempts) { - if (maxAllowedNoOfAttempts == null) { - throw new NullPointerException("maxAllowedNoOfAttempts should not be null"); - } + private final AtomicInteger currentNoOfAttempts = new AtomicInteger(0); + public NoOfAttemptsPullRetryPolicy(int maxAllowedNoOfAttempts) { if (maxAllowedNoOfAttempts < 0) { throw new IllegalArgumentException("maxAllowedNoOfAttempts should not be negative"); } @@ -33,6 +31,6 @@ public NoOfAttemptsPullRetryPolicy(Integer maxAllowedNoOfAttempts) { @Override public boolean shouldRetry(DockerImageName imageName, Throwable error) { - return ++currentNoOfAttempts <= maxAllowedNoOfAttempts; + return currentNoOfAttempts.incrementAndGet() <= maxAllowedNoOfAttempts; } } From 21b081dd9927f0643567dab3b6206374b4679629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Campanero=20Ortiz?= Date: Sun, 1 Mar 2026 13:51:06 +0100 Subject: [PATCH 13/15] feat: allow reusing the same retry policy instance --- .../images/retry/NoOfAttemptsPullRetryPolicy.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java index 367e7f47395..dae5a9224c7 100644 --- a/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java +++ b/core/src/main/java/org/testcontainers/images/retry/NoOfAttemptsPullRetryPolicy.java @@ -29,6 +29,11 @@ public NoOfAttemptsPullRetryPolicy(int maxAllowedNoOfAttempts) { this.maxAllowedNoOfAttempts = maxAllowedNoOfAttempts; } + @Override + public void pullStarted() { + currentNoOfAttempts.set(0); + } + @Override public boolean shouldRetry(DockerImageName imageName, Throwable error) { return currentNoOfAttempts.incrementAndGet() <= maxAllowedNoOfAttempts; From 520282a5edb13fdce9a6d1674a45f1f84aa4bda4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Campanero=20Ortiz?= Date: Sun, 1 Mar 2026 13:51:20 +0100 Subject: [PATCH 14/15] chore: minor tweaks and fixes --- .../images/retry/LimitedDurationPullRetryPolicy.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/testcontainers/images/retry/LimitedDurationPullRetryPolicy.java b/core/src/main/java/org/testcontainers/images/retry/LimitedDurationPullRetryPolicy.java index decab925260..008ee3b755c 100644 --- a/core/src/main/java/org/testcontainers/images/retry/LimitedDurationPullRetryPolicy.java +++ b/core/src/main/java/org/testcontainers/images/retry/LimitedDurationPullRetryPolicy.java @@ -7,6 +7,7 @@ import java.time.Duration; import java.time.Instant; +import java.util.Objects; /** * An ImagePullRetryPolicy which will retry a failed image pull if the time elapsed since the @@ -20,12 +21,10 @@ public class LimitedDurationPullRetryPolicy implements ImagePullRetryPolicy { @Getter private final Duration maxAllowedDuration; - Instant lastRetryAllowed; + private Instant lastRetryAllowed; public LimitedDurationPullRetryPolicy(Duration maxAllowedDuration) { - if (maxAllowedDuration == null) { - throw new NullPointerException("maxAllowedDuration should not be null"); - } + Objects.requireNonNull(maxAllowedDuration, "maxAllowedDuration should not be null"); if (maxAllowedDuration.isNegative()) { throw new IllegalArgumentException("maxAllowedDuration should not be negative"); From 82d706634d938ea231835f32f4b8438069c43e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Campanero=20Ortiz?= Date: Sun, 1 Mar 2026 14:16:37 +0100 Subject: [PATCH 15/15] test: use junit5 for testing and improve testing logic --- .../retry/ImagePullRetryPolicyTest.java | 44 +++++++------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/core/src/test/java/org/testcontainers/images/retry/ImagePullRetryPolicyTest.java b/core/src/test/java/org/testcontainers/images/retry/ImagePullRetryPolicyTest.java index a2c209cf210..5e193fe09d6 100644 --- a/core/src/test/java/org/testcontainers/images/retry/ImagePullRetryPolicyTest.java +++ b/core/src/test/java/org/testcontainers/images/retry/ImagePullRetryPolicyTest.java @@ -1,27 +1,17 @@ package org.testcontainers.images.retry; import com.github.dockerjava.api.exception.InternalServerErrorException; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.testcontainers.DockerRegistryContainer; +import org.junit.jupiter.api.Test; import org.testcontainers.utility.DockerImageName; import java.time.Duration; -import java.time.Instant; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class ImagePullRetryPolicyTest { - @ClassRule - public static DockerRegistryContainer registry = new DockerRegistryContainer(); - - private final DockerImageName imageName = registry.createImage(); - - @Rule - public ExpectedException thrown = ExpectedException.none(); + private final DockerImageName imageName = DockerImageName.parse("any/image:latest"); @Test public void shouldNotRetryWhenUsingFailFastPullRetryPolicy() { @@ -32,37 +22,37 @@ public void shouldNotRetryWhenUsingFailFastPullRetryPolicy() { @Test public void shouldFailIfTheConfiguredDurationIsNegativeWhenUsingLimitedDurationPullRetryPolicy() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("should not be negative"); - PullRetryPolicy.limitedDuration(Duration.ofMinutes(-1)); + assertThatThrownBy(() -> PullRetryPolicy.limitedDuration(Duration.ofMinutes(-1))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("should not be negative"); } @Test public void shouldFailIfPullStartedIsNotBeingCalledBeforeShouldRetryWhenUsingLimitedDurationPullRetryPolicy() { - thrown.expect(IllegalStateException.class); - thrown.expectMessage("Please, check that pullStarted has been called."); ImagePullRetryPolicy policy = PullRetryPolicy.limitedDuration(Duration.ofMinutes(1)); - policy.shouldRetry(imageName, new Exception()); + assertThatThrownBy(() -> policy.shouldRetry(imageName, new Exception())) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Please, check that pullStarted has been called."); } @Test - public void shouldRetryDuringTheConfiguredAmountOfTimeWhenUsingLimitedDurationPullRetryPolicy() { + public void shouldRetryDuringTheConfiguredAmountOfTimeWhenUsingLimitedDurationPullRetryPolicy() throws InterruptedException { Duration maxAllowedDuration = Duration.ofMillis(100); - Instant lastRetryAllowed = Instant.now().plus(maxAllowedDuration); ImagePullRetryPolicy policy = PullRetryPolicy.limitedDuration(maxAllowedDuration); policy.pullStarted(); - while (Instant.now().isBefore(lastRetryAllowed)) { - assertThat(policy.shouldRetry(imageName, new Exception())).isTrue(); - } + + assertThat(policy.shouldRetry(imageName, new Exception())).isTrue(); + + Thread.sleep(maxAllowedDuration.toMillis() + 50); assertThat(policy.shouldRetry(imageName, new Exception())).isFalse(); } @Test public void shouldFailIfTheConfiguredNumberOfAttemptsIsNegativeWhenUsingNoOfAttemptsPullRetryPolicy() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("should not be negative"); - PullRetryPolicy.noOfAttempts(-1); + assertThatThrownBy(() -> PullRetryPolicy.noOfAttempts(-1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("should not be negative"); } @Test