Skip to content

fix: return reaper handshake errors - #3841

Open
drawliin wants to merge 2 commits into
testcontainers:mainfrom
drawliin:fix/reaper-handshake-error
Open

fix: return reaper handshake errors#3841
drawliin wants to merge 2 commits into
testcontainers:mainfrom
drawliin:fix/reaper-handshake-error

Conversation

@drawliin

@drawliin drawliin commented Aug 6, 2026

Copy link
Copy Markdown

This supersedes #3828. The original PR was automatically closed after I accidentally deleted my fork

Related issues

What does this PR do?

This PR makes Reaper.connect perform the Ryuk handshake synchronously before returning a successful connection.

If the handshake fails, the connection is closed and the handshake error is returned to the caller. The existing retry path can then handle the failure instead of treating the reaper connection as successful.

It also adds a regression test using a local TCP listener that returns an invalid ACK, verifying that connect returns an error and no termination channel.

Why is it important?

Previously, handshake failures were only logged inside the connection goroutine. Reaper.connect still returned a non-nil termination channel and nil error, so callers could believe the reaper was connected even though Ryuk had rejected or failed the handshake.

Returning the error makes reaper startup failures visible and retryable.

How to test this PR

go test ./ -run "TestReaperConnectReturnsHandshakeError|TestSpawnerRetryError|TestSpawnerBackoff"

@drawliin
drawliin requested a review from a team as a code owner August 6, 2026 17:26
@netlify

netlify Bot commented Aug 6, 2026

Copy link
Copy Markdown

Deploy Preview for testcontainers-go ready!

Name Link
🔨 Latest commit 1ac9389
🔍 Latest deploy log https://app.netlify.com/projects/testcontainers-go/deploys/6a74c4c55ca98600082d7c47
😎 Deploy Preview https://deploy-preview-3841--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

    • Connection attempts now report handshake failures immediately.
    • Failed handshakes properly close the connection instead of leaving it active.
    • Prevented termination-signal handling from starting when the initial handshake fails.
  • Tests

    • Added coverage for invalid handshake responses and connection cleanup.

Walkthrough

Reaper.connect now completes the handshake before returning. It closes the connection and returns an error when the handshake fails. Tests cover invalid handshake responses and use Reaper.Connect for successful connections.

Changes

Reaper handshake handling

Layer / File(s) Summary
Synchronous handshake failure handling
reaper.go, reaper_test.go
Reaper.connect now performs the handshake before starting termination handling. Failed handshakes close the connection and return wrapped errors. Tests validate invalid responses and successful connections through Reaper.Connect.

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

Possibly related PRs

Suggested labels: bug

Suggested reviewers: mdelapenya

Poem

I’m a rabbit guarding the reaper’s door,
No failed handshake slips through anymore.
Bad replies close the connection tight,
Clear errors guide the retry’s flight.
Tests hop after, and all is right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: returning reaper handshake errors.
Description check ✅ Passed The description directly explains the synchronous handshake change, error handling, regression test, and retry behavior.
Linked Issues check ✅ Passed The changes satisfy issue #3827 by making failed handshakes connection failures and adding regression coverage.
Out of Scope Changes check ✅ Passed All code and test changes support the linked issue and stated objective; no unrelated changes are present.
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
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.go`:
- Around line 545-548: Update Connect around r.handshake(conn) to apply the
context deadline to the connection before starting the synchronous handshake, so
an unresponsive peer cannot block indefinitely. After a successful handshake,
clear the connection deadline before returning the connection; preserve existing
close-and-error behavior when the handshake fails.
🪄 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: 42e15ab9-e889-405f-8ede-52842dd30e3e

📥 Commits

Reviewing files that changed from the base of the PR and between 0cfd2f9 and 07891a3.

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

Comment thread reaper.go
Comment on lines +545 to +548
if err := r.handshake(conn); err != nil {
conn.Close()
return nil, fmt.Errorf("handshake reaper %s: %w", r.Endpoint, err)
}

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect whether the handshake path applies a connection deadline.
rg -n -C 5 'DialContext|SetDeadline|handshake\(conn\)' reaper.go reaper_test.go

Repository: testcontainers/testcontainers-go

Length of output: 915


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- outline reaper.go ---\n'
ast-grep outline reaper.go --match connect --view expanded || true

printf '\n--- relevant reaper.go sections ---\n'
sed -n '500,580p' reaper.go | cat -n | sed 's/^/reaper.go /'

printf '\n--- handshake implementation references ---\n'
rg -n -C 8 'func \(r \*Reaper\) handshake|handshake\(' reaper.go

printf '\n--- connect/call references ---\n'
rg -n -C 7 'connect\(ctx\)|handshake\(conn\)|DialContext|ConfigContext|Connect' reaper.go reaper_test.go

printf '\n--- Go net Dialer docs/source in local tooling if available ---\n'
go env GOPATH GOROOT 2>/dev/null || true
mkdir -p /tmp/gosrc
GOSRC="$GOROOT/src"
[ -n "$GOSRC" ] && { rg -n 'func \(d \*Dialer\) DialContext|ctx\.Apply|SetDeadline' "$GOSRC/net" || true; }

Repository: testcontainers/testcontainers-go

Length of output: 261


🌐 Web query:

Go net.Dialer.DialContext context timeout applies to dial only net.Conn Read Write deadline

💡 Result:

No, the context timeout passed to net.Dialer.DialContext does not apply to Read or Write operations on the established connection [1][2][3]. The context provided to DialContext is used exclusively for the connection establishment phase (the dial) [1][2][4]. Once the connection has been successfully established, the context's expiration or cancellation has no further effect on the connection [1][4]. To control timeouts for Read and Write operations after a connection is established, you must explicitly set deadlines on the net.Conn object using the SetDeadline, SetReadDeadline, or SetWriteDeadline methods [5][3]. Summary of behavior: 1. Dial Phase: The timeout is governed by the provided context and the Dialer's Timeout field [1][4]. If the context expires or is canceled during this time, the dial operation is aborted [1][4]. 2. Established Connection Phase: The context is no longer active for the connection [1][3]. You must manage I/O timeouts manually using the connection's deadline methods [5][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- relevant file and function info ---'
wc -l reaper.go reaper_test.go
rg -n -C 8 'func \(r \*Reaper\) connect|func \(r \*Reaper\) handshake|func \(r \*Reaper\) Connect|DialContext|handshake\(conn\)|SetDeadline|SetReadDeadline|SetWriteDeadline' reaper.go reaper_test.go

echo '--- connect section ---'
sed -n '515,570p' reaper.go

echo '--- handshake section ---'
rg -n 'handshake' reaper.go
awk '/func \(r \*Reaper\) handshake/ {p=1; f=NR} p; NR>=f && /^[[:space:]]*$/ && NR>f+1 {exit}' reaper.go | sed -n '1,160p'

Repository: testcontainers/testcontainers-go

Length of output: 4534


🌐 Web query:

Go net.Dialer.DialContext context deadline cancels only dial TCP connection established Read deadline

💡 Result:

The context passed to net.Dialer.DialContext is used exclusively for the connection establishment phase [1][2][3]. Once a TCP connection is successfully established, any expiration or cancellation of that context has no effect on the connection's lifetime or its I/O operations [1][4]. Specifically, the net.Dialer.DialContext mechanism does not bind the context to the connection after it is returned [1]. If the context expires or is canceled after the dial has finished, the established net.Conn remains open and will continue to operate normally [1][4]. Consequently, the context does not automatically enforce any read or write deadlines on the established connection [5][2]. To enforce timeouts on an established connection, you must explicitly manage the connection's deadline using the net.Conn.SetDeadline, SetReadDeadline, or SetWriteDeadline methods [1][6][2]. Because these methods require a concrete connection object, a common pattern for handling timeouts on established connections is to wrap the net.Conn or to invoke these deadline methods directly after obtaining the connection [2][7]. Top results: [1][5][2][4]

Citations:


Bound the synchronous handshake by the context deadline.

Connect applies the five-second timeout only to net.Dialer.DialContext; once the TCP connection is returned, r.handshake(conn) is no longer limited by that context. If Ryuk accepts the connection and does not send an ACK, the handshake can block indefinitely and prevent retries. Set a connection deadline before the handshake and clear it after the handshake succeeds.

🤖 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.go` around lines 545 - 548, Update Connect around r.handshake(conn) to
apply the context deadline to the connection before starting the synchronous
handshake, so an unresponsive peer cannot block indefinitely. After a successful
handshake, clear the connection deadline before returning the connection;
preserve existing close-and-error behavior when the handshake fails.

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.

[Bug]: Reaper.connect swallows a failed handshake, leaving the process silently unregistered with Ryuk

1 participant