Skip to content

fix: bound the reaper connect dial so the spawner backoff can retry - #3839

Open
ghaering wants to merge 1 commit into
testcontainers:mainfrom
ghaering:fix/reaper-dial-timeout
Open

fix: bound the reaper connect dial so the spawner backoff can retry#3839
ghaering wants to merge 1 commit into
testcontainers:mainfrom
ghaering:fix/reaper-dial-timeout

Conversation

@ghaering

@ghaering ghaering commented Aug 6, 2026

Copy link
Copy Markdown

What does this PR do?

Bound the dial in Reaper.connect() to 5s, same as Connect(). Add a
regression test.

Why is it important?

The spawner's connect check dials with the caller's context, which has
no deadline. With dropped SYNs (we hit a conntrack/DNAT race under
container churn) the dial sits in the kernel's ~2min connect timeout.
One attempt eats the whole 20s backoff budget, so the ETIMEDOUT retry
never happens. With a 5s bound the retry dials from a fresh source port
(fresh conntrack entry) and connects.

Related issues

  • None that I know of.

How to test this PR

go test -run TestReaperConnectDialBounded .

Dials 192.0.2.1:8080 (TEST-NET-1, not routed), checks the dial fails
within the bound and is classified retryable. Without the fix it takes
~2min and fails. On networks that answer for TEST-NET-1 the dial fails
instantly, so the test passes without exercising the timeout path.

Follow-ups

Later maybe: blackhole locally with a backlog-0 listener (Linux only)
instead of relying on TEST-NET-1.

connect() dialed with the caller's context, which has no deadline. With
dropped SYNs (conntrack/DNAT race under container churn) the dial sits
in the kernel's ~2min connect timeout. One attempt eats the whole 20s
backoff budget, so the ETIMEDOUT retry never happens.

Bound the dial to 5s like Connect() does. The retry dials from a fresh
source port and gets a fresh conntrack entry.
@ghaering
ghaering requested a review from a team as a code owner August 6, 2026 17:23
@netlify

netlify Bot commented Aug 6, 2026

Copy link
Copy Markdown

Deploy Preview for testcontainers-go ready!

Name Link
🔨 Latest commit 4f817f3
🔍 Latest deploy log https://app.netlify.com/projects/testcontainers-go/deploys/6a74c33164434c0008fa6d80
😎 Deploy Preview https://deploy-preview-3839--testcontainers-go.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 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • Bug Fixes
    • Added a five-second connection timeout when connecting to the Reaper service.
    • Prevented unreachable endpoints from delaying connection attempts indefinitely.
    • Preserved retry behavior for connection failures and backoff handling.

Walkthrough

Reaper.connect now limits TCP dialing to five seconds before the handshake. A new test verifies that unreachable endpoints fail within the expected bound and remain eligible for spawner retries.

Changes

Reaper dial timeout

Layer / File(s) Summary
Bounded dialing and retry validation
reaper.go, reaper_dial_bound_test.go
Reaper.connect uses a five-second child context for TCP dialing. The test checks timely failure and confirms that reaperSpawner.retryError does not classify the error as permanent.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested labels: bug

Suggested reviewers: mdelapenya, stevenh

Poem

I bounded the dial with a timer so neat,
Five seconds keeps retries on beat.
If Reaper is away,
We try another day—
Hop, hop, the connection repeats! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description explains the 5-second dial bound, its retry purpose, regression test, and testing method.
Title check ✅ Passed The title clearly identifies the primary change: bounding the Reaper connection dial to enable spawner retries.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@reaper_dial_bound_test.go`:
- Around line 15-30: Make the connect timeout regression test deterministic by
injecting a dialer or dial function into Reaper.connect that blocks until the
derived child context expires, rather than dialing 192.0.2.1. Assert that the
returned error is a context deadline or timeout error, retain the retryError
classification check, and tighten the elapsed-time assertion to five seconds
plus a small scheduling margin.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 60d5251c-1cb1-4c92-90a9-00f22d2b5075

📥 Commits

Reviewing files that changed from the base of the PR and between 0cfd2f9 and 4f817f3.

📒 Files selected for processing (2)
  • reaper.go
  • reaper_dial_bound_test.go

Comment thread reaper_dial_bound_test.go
Comment on lines +15 to +30
r := &Reaper{Endpoint: "192.0.2.1:8080"} // TEST-NET-1, not routed
start := time.Now()
_, err := r.connect(context.Background())
elapsed := time.Since(start)

if err == nil {
t.Fatal("expected dial error")
}
if elapsed > 10*time.Second {
t.Fatalf("dial not bounded: took %v", elapsed)
}
s := &reaperSpawner{}
rerr := s.retryError(err)
var perm *backoff.PermanentError
if errors.As(rerr, &perm) {
t.Fatalf("timeout classified permanent, backoff would stop: %v", rerr)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Make the timeout regression test deterministic and enforce the five-second bound.

192.0.2.1 does not guarantee a SYN timeout on every CI host. The dial can return ENETUNREACH or ECONNREFUSED immediately. reaper.go Lines 237-248 can classify ENETUNREACH as permanent, and an immediate ECONNREFUSED lets an implementation without the new timeout pass this test.

Inject the dialer or dial function and block it until the child context expires. Assert a deadline or timeout error. Set the elapsed limit to five seconds plus a small scheduling margin instead of ten seconds.

🤖 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 `@reaper_dial_bound_test.go` around lines 15 - 30, Make the connect timeout
regression test deterministic by injecting a dialer or dial function into
Reaper.connect that blocks until the derived child context expires, rather than
dialing 192.0.2.1. Assert that the returned error is a context deadline or
timeout error, retain the retryError classification check, and tighten the
elapsed-time assertion to five seconds plus a small scheduling margin.

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.

1 participant