Skip to content

Commit 309dec1

Browse files
authored
fix: parallel containers clean race (testcontainers#2790)
Simply the logic in parallel containers, eliminating a clean up race condition where multiple clean ups on the same container could occur at the same time.
1 parent 1d01e21 commit 309dec1

File tree

2 files changed

+39
-60
lines changed

2 files changed

+39
-60
lines changed

parallel.go

Lines changed: 29 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package testcontainers
22

33
import (
44
"context"
5-
"errors"
65
"fmt"
76
"sync"
87
)
@@ -32,24 +31,27 @@ func (gpe ParallelContainersError) Error() string {
3231
return fmt.Sprintf("%v", gpe.Errors)
3332
}
3433

34+
// parallelContainersResult represents result.
35+
type parallelContainersResult struct {
36+
ParallelContainersRequestError
37+
Container Container
38+
}
39+
3540
func parallelContainersRunner(
3641
ctx context.Context,
3742
requests <-chan GenericContainerRequest,
38-
errorsCh chan<- ParallelContainersRequestError,
39-
containers chan<- Container,
43+
results chan<- parallelContainersResult,
4044
wg *sync.WaitGroup,
4145
) {
4246
defer wg.Done()
4347
for req := range requests {
4448
c, err := GenericContainer(ctx, req)
49+
res := parallelContainersResult{Container: c}
4550
if err != nil {
46-
errorsCh <- ParallelContainersRequestError{
47-
Request: req,
48-
Error: errors.Join(err, TerminateContainer(c)),
49-
}
50-
continue
51+
res.Request = req
52+
res.Error = err
5153
}
52-
containers <- c
54+
results <- res
5355
}
5456
}
5557

@@ -65,41 +67,26 @@ func ParallelContainers(ctx context.Context, reqs ParallelContainerRequest, opt
6567
}
6668

6769
tasksChan := make(chan GenericContainerRequest, tasksChanSize)
68-
errsChan := make(chan ParallelContainersRequestError)
69-
resChan := make(chan Container)
70-
waitRes := make(chan struct{})
71-
72-
containers := make([]Container, 0)
73-
errors := make([]ParallelContainersRequestError, 0)
70+
resultsChan := make(chan parallelContainersResult, tasksChanSize)
71+
done := make(chan struct{})
7472

75-
wg := sync.WaitGroup{}
73+
var wg sync.WaitGroup
7674
wg.Add(tasksChanSize)
7775

7876
// run workers
7977
for i := 0; i < tasksChanSize; i++ {
80-
go parallelContainersRunner(ctx, tasksChan, errsChan, resChan, &wg)
78+
go parallelContainersRunner(ctx, tasksChan, resultsChan, &wg)
8179
}
8280

81+
var errs []ParallelContainersRequestError
82+
containers := make([]Container, 0, len(reqs))
8383
go func() {
84-
for {
85-
select {
86-
case c, ok := <-resChan:
87-
if !ok {
88-
resChan = nil
89-
} else {
90-
containers = append(containers, c)
91-
}
92-
case e, ok := <-errsChan:
93-
if !ok {
94-
errsChan = nil
95-
} else {
96-
errors = append(errors, e)
97-
}
98-
}
99-
100-
if resChan == nil && errsChan == nil {
101-
waitRes <- struct{}{}
102-
break
84+
defer close(done)
85+
for res := range resultsChan {
86+
if res.Error != nil {
87+
errs = append(errs, res.ParallelContainersRequestError)
88+
} else {
89+
containers = append(containers, res.Container)
10390
}
10491
}
10592
}()
@@ -108,14 +95,15 @@ func ParallelContainers(ctx context.Context, reqs ParallelContainerRequest, opt
10895
tasksChan <- req
10996
}
11097
close(tasksChan)
98+
11199
wg.Wait()
112-
close(resChan)
113-
close(errsChan)
114100

115-
<-waitRes
101+
close(resultsChan)
102+
103+
<-done
116104

117-
if len(errors) != 0 {
118-
return containers, ParallelContainersError{Errors: errors}
105+
if len(errs) != 0 {
106+
return containers, ParallelContainersError{Errors: errs}
119107
}
120108

121109
return containers, nil

parallel_test.go

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package testcontainers
22

33
import (
44
"context"
5-
"errors"
65
"fmt"
76
"testing"
87
"time"
@@ -99,23 +98,18 @@ func TestParallelContainers(t *testing.T) {
9998
for _, tc := range tests {
10099
t.Run(tc.name, func(t *testing.T) {
101100
res, err := ParallelContainers(context.Background(), tc.reqs, ParallelContainersOptions{})
102-
if err != nil {
103-
require.NotZero(t, tc.expErrors)
104-
var e ParallelContainersError
105-
errors.As(err, &e)
106-
if len(e.Errors) != tc.expErrors {
107-
t.Fatalf("expected errors: %d, got: %d\n", tc.expErrors, len(e.Errors))
108-
}
109-
}
110-
111101
for _, c := range res {
112-
c := c
113102
CleanupContainer(t, c)
114103
}
115104

116-
if len(res) != tc.resLen {
117-
t.Fatalf("expected containers: %d, got: %d\n", tc.resLen, len(res))
105+
if tc.expErrors != 0 {
106+
require.Error(t, err)
107+
var errs ParallelContainersError
108+
require.ErrorAs(t, err, &errs)
109+
require.Len(t, errs.Errors, tc.expErrors)
118110
}
111+
112+
require.Len(t, res, tc.resLen)
119113
})
120114
}
121115
}
@@ -157,11 +151,8 @@ func TestParallelContainersWithReuse(t *testing.T) {
157151
ctx := context.Background()
158152

159153
res, err := ParallelContainers(ctx, parallelRequest, ParallelContainersOptions{})
160-
if err != nil {
161-
var e ParallelContainersError
162-
errors.As(err, &e)
163-
t.Fatalf("expected errors: %d, got: %d\n", 0, len(e.Errors))
154+
for _, c := range res {
155+
CleanupContainer(t, c)
164156
}
165-
// Container is reused, only terminate first container
166-
CleanupContainer(t, res[0])
157+
require.NoError(t, err)
167158
}

0 commit comments

Comments
 (0)