|
| 1 | +package compose |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "testing" |
| 6 | + |
| 7 | + "github.com/stretchr/testify/require" |
| 8 | + "go.uber.org/goleak" |
| 9 | +) |
| 10 | + |
| 11 | +// TestDockerComposeGoroutineLeak verifies that calling Up() followed by Down() |
| 12 | +// and Close() does not leak net/http persistConn goroutines from the internal |
| 13 | +// Docker CLI HTTP transport. |
| 14 | +// |
| 15 | +// Before the fix, two goroutines per Up+Down cycle were leaked permanently: |
| 16 | +// |
| 17 | +// net/http.(*persistConn).readLoop |
| 18 | +// net/http.(*persistConn).writeLoop |
| 19 | +// |
| 20 | +// Note: Reaper (Ryuk) goroutines are intentionally excluded from this check; |
| 21 | +// they are a separate pre-existing issue tracked in the same issue but require |
| 22 | +// a distinct fix involving Reaper termination signal handling in Down(). |
| 23 | +func TestDockerComposeGoroutineLeak(t *testing.T) { |
| 24 | + path, _ := RenderComposeSimple(t) |
| 25 | + |
| 26 | + compose, err := NewDockerCompose(path) |
| 27 | + require.NoError(t, err, "NewDockerCompose()") |
| 28 | + |
| 29 | + // Snapshot goroutines after NewDockerCompose establishes its initial |
| 30 | + // Docker provider connection, so IgnoreCurrent covers the provider's |
| 31 | + // keep-alive transport goroutines that pre-date the compose Up/Down cycle. |
| 32 | + ignoreExisting := goleak.IgnoreCurrent() |
| 33 | + |
| 34 | + // Register Close cleanup first so it runs last (t.Cleanup is LIFO). |
| 35 | + // goleak.VerifyNone is called here so it runs after both Down and Close. |
| 36 | + t.Cleanup(func() { |
| 37 | + require.NoError(t, compose.Close(), "compose.Close()") |
| 38 | + goleak.VerifyNone(t, |
| 39 | + ignoreExisting, |
| 40 | + // TODO(#2008): Remove this ignore when the Reaper goroutine leak is fixed. |
| 41 | + // This references an internal anonymous closure that may change if Reaper is refactored. |
| 42 | + goleak.IgnoreTopFunction("github.com/testcontainers/testcontainers-go.(*Reaper).connect.func1"), |
| 43 | + ) |
| 44 | + }) |
| 45 | + |
| 46 | + ctx := context.Background() |
| 47 | + |
| 48 | + require.NoError(t, compose.Up(ctx, Wait(true)), "compose.Up()") |
| 49 | + |
| 50 | + // Register Down cleanup after Up so it runs before Close (t.Cleanup is LIFO). |
| 51 | + // This ensures Down is called even if the test panics after Up succeeds. |
| 52 | + t.Cleanup(func() { |
| 53 | + require.NoError(t, compose.Down(ctx, RemoveOrphans(true), RemoveVolumes(true)), "compose.Down()") |
| 54 | + }) |
| 55 | +} |
0 commit comments