Skip to content

fix: Retry transient Docker image pull failures - #1734

Open
thomhurst wants to merge 1 commit into
testcontainers:developfrom
thomhurst:agent/retry-image-pulls
Open

fix: Retry transient Docker image pull failures#1734
thomhurst wants to merge 1 commit into
testcontainers:developfrom
thomhurst:agent/retry-image-pulls

Conversation

@thomhurst

@thomhurst thomhurst commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds a bounded retry policy around Docker image pulls:

  • tries at most three times;
  • retries DockerApiException responses for HTTP 408, 429, and 5xx statuses;
  • retries transport, I/O, socket, timeout, and transport-originated cancellation failures;
  • uses exponential backoff (1s, 2s) with up to 25% jitter;
  • immediately preserves caller cancellation and permanent Docker API failures;
  • logs each retry with image, attempt, delay, and a sanitized reason that excludes the Docker response body.

The policy's delay functions are isolated internally so its behavior is deterministic and fast to unit test. Docker.DotNet's DockerApiException exposes the status code and response body, but not response headers, so Retry-After is not available at this layer.

Why is it important?

A short-lived registry or network failure currently aborts the image pull and the entire test session. Retrying only clearly transient failures reduces CI flakes without masking invalid images, missing manifests, or authentication/authorization failures.

Related issues

How to test this PR

  • dotnet build src/Testcontainers/Testcontainers.csproj --configuration Release --no-restore (all five target frameworks; zero warnings)
  • dotnet test tests/Testcontainers.Tests/Testcontainers.Tests.csproj --configuration Release --no-restore (566 passed, 1 unrelated skipped)
  • focused retry suite: 29 passed, covering success, every retry category, permanent failures, exhaustion, backoff/jitter, cancellation, and sanitized logging

Summary by CodeRabbit

  • Reliability

    • Docker image pulls now automatically retry transient failures up to three times.
    • Retries use increasing delays with jitter and respect cancellation requests.
    • Permanent failures and caller-requested cancellations stop immediately.
  • Logging

    • Warning messages now show retry attempts, delays, image details, and failure reasons.
  • Testing

    • Added coverage for retry behavior, cancellation, backoff timing, success, and failure scenarios.

Bound retries to three attempts, preserve caller cancellation, and avoid retrying permanent Docker API responses.

Refs testcontainers#1733
@thomhurst
thomhurst requested a review from HofmeisterAn as a code owner August 8, 2026 22:08
@netlify

netlify Bot commented Aug 8, 2026

Copy link
Copy Markdown

Deploy Preview for testcontainers-dotnet ready!

Name Link
🔨 Latest commit c770a6d
🔍 Latest deploy log https://app.netlify.com/projects/testcontainers-dotnet/deploys/6a77a8e528d7a10008164415
😎 Deploy Preview https://deploy-preview-1734--testcontainers-dotnet.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Docker image pulls now use a bounded retry policy for transient failures. The policy applies exponential backoff with jitter, logs retry details, honors cancellation, and reports sanitized failure reasons. Unit tests cover retry, failure, delay, and cancellation behavior.

Changes

Docker image pull retry flow

Layer / File(s) Summary
Retry policy implementation
src/Testcontainers/Clients/DockerImagePullRetryPolicy.cs
Adds bounded retries for transient Docker and transport failures, with exponential jitter, cancellation handling, retry callbacks, and sanitized failure reasons.
Pull integration and retry logging
src/Testcontainers/Clients/DockerImageOperations.cs, src/Testcontainers/Logging.cs
Routes image creation through the retry policy and logs image name, attempt counts, delay, and retry reason.
Retry policy validation
tests/Testcontainers.Tests/Unit/Clients/DockerImagePullRetryPolicyTest.cs
Tests success, retry limits, exception classification, cancellation, backoff jitter, and sanitized Docker API failure reporting.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DockerImageOperations
  participant DockerImagePullRetryPolicy
  participant DockerAPI
  participant ILogger
  DockerImageOperations->>DockerImagePullRetryPolicy: ExecuteAsync image pull
  DockerImagePullRetryPolicy->>DockerAPI: CreateImageAsync
  DockerAPI-->>DockerImagePullRetryPolicy: success or transient failure
  DockerImagePullRetryPolicy->>ILogger: log retry attempt and reason
  DockerImagePullRetryPolicy->>DockerAPI: retry after delayed backoff
Loading

Suggested labels: enhancement

Suggested reviewers: hofmeisteran

Poem

I’m a rabbit who guards each pull,
With jittered hops when nets are full.
Three tries through Docker’s cloudy wall,
Safe logs record each rise and fall.
Cancellation stops my trail—
Then carrots wait when pulls prevail.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: retrying transient Docker image pull failures.
Description check ✅ Passed The description includes all mandatory sections and clearly explains the implementation, rationale, related issue, and test commands.
Linked Issues check ✅ Passed The implementation satisfies issue #1733 by adding bounded retries, backoff, transient-failure classification, cancellation, and sanitized retry logging.
Out of Scope Changes check ✅ Passed The changes are limited to Docker image pull retry behavior, logging, and focused unit tests required by issue #1733.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/Testcontainers/Clients/DockerImagePullRetryPolicy.cs (2)

64-67: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider inspecting inner exceptions for transport failures.

The checks match only the outermost exception type. HttpRequestException often wraps a SocketException, and Docker.DotNet can surface an IOException nested inside another exception. If the transport failure arrives wrapped in a type that is not listed, the pull is not retried.

A small traversal of InnerException would cover those cases.

♻️ Proposed change
-      return exception is HttpRequestException
-        || exception is IOException
-        || exception is SocketException
-        || exception is TimeoutException;
+      for (var current = exception; current != null; current = current.InnerException)
+      {
+        if (current is HttpRequestException || current is IOException || current is SocketException || current is TimeoutException)
+        {
+          return true;
+        }
+      }
+
+      return false;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Testcontainers/Clients/DockerImagePullRetryPolicy.cs` around lines 64 -
67, Update the retry classification logic in DockerImagePullRetryPolicy to
traverse each exception’s InnerException chain and return true when any nested
exception is an HttpRequestException, IOException, SocketException, or
TimeoutException; otherwise preserve the existing non-retry result.

51-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use one consistent comparison style for the status code.

Line 54 calls Equals on a boxed enum. Line 55 uses == with a cast. Line 56 uses the int value. The already-computed statusCode int makes all three checks uniform.

♻️ Proposed simplification
       if (exception is DockerApiException dockerApiException)
       {
         var statusCode = (int)dockerApiException.StatusCode;
-        return HttpStatusCode.RequestTimeout.Equals(dockerApiException.StatusCode)
-          || (HttpStatusCode)429 == dockerApiException.StatusCode
-          || statusCode >= 500 && statusCode <= 599;
+        return 408 == statusCode
+          || 429 == statusCode
+          || (statusCode >= 500 && statusCode <= 599);
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Testcontainers/Clients/DockerImagePullRetryPolicy.cs` around lines 51 -
57, Update the status-code checks in the DockerApiException branch of the retry
policy to use the existing statusCode integer consistently: compare it with the
numeric values for RequestTimeout and TooManyRequests, while preserving the
existing 5xx range behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/Testcontainers/Clients/DockerImagePullRetryPolicy.cs`:
- Around line 64-67: Update the retry classification logic in
DockerImagePullRetryPolicy to traverse each exception’s InnerException chain and
return true when any nested exception is an HttpRequestException, IOException,
SocketException, or TimeoutException; otherwise preserve the existing non-retry
result.
- Around line 51-57: Update the status-code checks in the DockerApiException
branch of the retry policy to use the existing statusCode integer consistently:
compare it with the numeric values for RequestTimeout and TooManyRequests, while
preserving the existing 5xx range behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7b3c1feb-bfae-4773-a67b-3b6ddc7850a0

📥 Commits

Reviewing files that changed from the base of the PR and between 1d329ad and c770a6d.

📒 Files selected for processing (4)
  • src/Testcontainers/Clients/DockerImageOperations.cs
  • src/Testcontainers/Clients/DockerImagePullRetryPolicy.cs
  • src/Testcontainers/Logging.cs
  • tests/Testcontainers.Tests/Unit/Clients/DockerImagePullRetryPolicyTest.cs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Enhancement]: Retry transient Docker image pull failures

1 participant