diff --git a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java index 7aff7918f6d..257bacc9961 100644 --- a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java +++ b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java @@ -3,6 +3,7 @@ import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.ListImagesCmd; import com.github.dockerjava.api.exception.DockerClientException; +import com.github.dockerjava.api.exception.InternalServerErrorException; import com.github.dockerjava.api.model.Image; import com.github.dockerjava.core.command.PullImageResultCallback; import lombok.NonNull; @@ -14,6 +15,8 @@ import org.testcontainers.utility.DockerLoggerFactory; import org.testcontainers.utility.LazyFuture; +import java.time.Duration; +import java.time.Instant; import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -29,6 +32,7 @@ public class RemoteDockerImage extends LazyFuture { */ @Deprecated public static final Set AVAILABLE_IMAGE_NAME_CACHE = new HashSet<>(); + private static final Duration PULL_RETRY_TIME_LIMIT = Duration.ofMinutes(2); private DockerImageName imageName; @@ -73,23 +77,34 @@ protected final String resolve() { return imageName.toString(); } + // The image is not available locally - pull it logger.info("Pulling docker image: {}. Please be patient; this may take some time but only needs to be done once.", imageName); - // The image is not available locally - pull it - try { - final PullImageResultCallback callback = new TimeLimitedLoggedPullImageResultCallback(logger); - dockerClient - .pullImageCmd(imageName.getUnversionedPart()) - .withTag(imageName.getVersionPart()) - .exec(callback); - callback.awaitCompletion(); - AVAILABLE_IMAGE_NAME_CACHE.add(imageName); - } catch (Exception e) { - logger.error("Failed to pull image: {}. Please check output of `docker pull {}`", imageName, imageName); - throw new ContainerFetchException("Failed to pull image: " + imageName, e); + Exception lastFailure = null; + final Instant lastRetryAllowed = Instant.now().plus(PULL_RETRY_TIME_LIMIT); + + while (Instant.now().isBefore(lastRetryAllowed)) { + try { + final PullImageResultCallback callback = new TimeLimitedLoggedPullImageResultCallback(logger); + dockerClient + .pullImageCmd(imageName.getUnversionedPart()) + .withTag(imageName.getVersionPart()) + .exec(callback); + callback.awaitCompletion(); + AVAILABLE_IMAGE_NAME_CACHE.add(imageName); + + return imageName.toString(); + } catch (InterruptedException | InternalServerErrorException e) { + // these classes of exception often relate to timeout/connection errors so should be retried + lastFailure = e; + logger.warn("Retrying pull for image: {} ({}s remaining)", + imageName, + Duration.between(Instant.now(), lastRetryAllowed).getSeconds()); + } } + logger.error("Failed to pull image: {}. Please check output of `docker pull {}`", imageName, imageName, lastFailure); - return imageName.toString(); + throw new ContainerFetchException("Failed to pull image: " + imageName, lastFailure); } catch (DockerClientException e) { throw new ContainerFetchException("Failed to get Docker client for " + imageName, e); }