Skip to content

Commit f4c09f3

Browse files
committed
Do not create or destroy containers from multiple threads
1 parent 7380430 commit f4c09f3

File tree

1 file changed

+101
-66
lines changed

1 file changed

+101
-66
lines changed

test/octopus_container_test_framework.go

Lines changed: 101 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"path/filepath"
2222
"strconv"
2323
"strings"
24+
"sync"
2425
"testing"
2526
"time"
2627
)
@@ -56,6 +57,7 @@ func (g *TestLogConsumer) Accept(l testcontainers.Log) {
5657
}
5758

5859
type OctopusContainerTest struct {
60+
mu sync.Mutex
5961
}
6062

6163
func (o *OctopusContainerTest) enableContainerLogging(container testcontainers.Container, ctx context.Context) error {
@@ -231,6 +233,55 @@ func (o *OctopusContainerTest) setupOctopus(ctx context.Context, connString stri
231233
return &OctopusContainer{Container: container, URI: uri}, nil
232234
}
233235

236+
// createDockerInfrastructure attemptes to create the complete Docker stack containing a
237+
// network, MSSQL container, and Octopus container. The return values include as much of
238+
// the partial stack as possible in the case of an error.
239+
func (o *OctopusContainerTest) createDockerInfrastructure(t *testing.T, ctx context.Context) (testcontainers.Network, *OctopusContainer, *mysqlContainer, error) {
240+
241+
network, networkName, err := o.setupNetwork(ctx)
242+
if err != nil {
243+
return nil, nil, nil, err
244+
}
245+
246+
sqlServer, err := o.setupDatabase(ctx, networkName)
247+
if err != nil {
248+
return network, nil, sqlServer, err
249+
}
250+
251+
sqlIp, err := sqlServer.Container.ContainerIP(ctx)
252+
if err != nil {
253+
return network, nil, sqlServer, err
254+
}
255+
256+
sqlName, err := sqlServer.Container.Name(ctx)
257+
if err != nil {
258+
return network, nil, sqlServer, err
259+
}
260+
261+
t.Log("SQL Server IP: " + sqlIp)
262+
t.Log("SQL Server Container Name: " + sqlName)
263+
264+
octopusContainer, err := o.setupOctopus(ctx, "Server="+sqlIp+",1433;Database=OctopusDeploy;User=sa;Password=Password01!", networkName, t)
265+
if err != nil {
266+
return network, octopusContainer, sqlServer, err
267+
}
268+
269+
octoIp, err := octopusContainer.Container.ContainerIP(ctx)
270+
if err != nil {
271+
return network, octopusContainer, sqlServer, err
272+
}
273+
274+
octoName, err := octopusContainer.Container.Name(ctx)
275+
if err != nil {
276+
return network, octopusContainer, sqlServer, err
277+
}
278+
279+
t.Log("Octopus IP: " + octoIp)
280+
t.Log("Octopus Container Name: " + octoName)
281+
282+
return network, octopusContainer, sqlServer, nil
283+
}
284+
234285
// ArrangeTest is wrapper that initialises Octopus, runs a test, and cleans up the containers
235286
func (o *OctopusContainerTest) ArrangeTest(t *testing.T, testFunc func(t *testing.T, container *OctopusContainer, client *client.Client) error) {
236287
err := retry.Do(
@@ -242,93 +293,77 @@ func (o *OctopusContainerTest) ArrangeTest(t *testing.T, testFunc func(t *testin
242293

243294
ctx := context.Background()
244295

245-
network, networkName, err := o.setupNetwork(ctx)
246-
if err != nil {
247-
return err
248-
}
249-
250-
sqlServer, err := o.setupDatabase(ctx, networkName)
251-
if err != nil {
252-
return err
253-
}
296+
// I don't think test containers are thread safe - parallel tests
297+
// frequently show that multiple tests access the same containers.
298+
// So only one thread can create a stack at a time
299+
o.mu.Lock()
300+
network, octopusContainer, sqlServer, err := o.createDockerInfrastructure(t, ctx)
301+
o.mu.Unlock()
254302

255-
sqlIp, err := sqlServer.Container.ContainerIP(ctx)
256-
if err != nil {
257-
return err
258-
}
303+
// Attempt to clean up whatever resources were created.
304+
// Don't return errors for the cleanup, just report them
305+
defer func() {
306+
o.mu.Lock()
307+
defer o.mu.Unlock()
259308

260-
sqlName, err := sqlServer.Container.Name(ctx)
261-
if err != nil {
262-
return err
263-
}
309+
stopTime := 1 * time.Minute
264310

265-
t.Log("SQL Server IP: " + sqlIp)
266-
t.Log("SQL Server Container Name: " + sqlName)
311+
if octopusContainer != nil {
312+
// This fixes the "can not get logs from container which is dead or marked for removal" error
313+
// See https://github.com/testcontainers/testcontainers-go/issues/606
314+
if os.Getenv("OCTODISABLEOCTOCONTAINERLOGGING") != "true" {
315+
stopProducerErr := octopusContainer.StopLogProducer()
267316

268-
octopusContainer, err := o.setupOctopus(ctx, "Server="+sqlIp+",1433;Database=OctopusDeploy;User=sa;Password=Password01!", networkName, t)
269-
if err != nil {
270-
return err
271-
}
317+
// try to continue on if there was an error stopping the producer
318+
if stopProducerErr != nil {
319+
t.Log(stopProducerErr)
320+
}
321+
}
272322

273-
octoIp, err := octopusContainer.Container.ContainerIP(ctx)
274-
if err != nil {
275-
return err
276-
}
323+
// Stop the containers
324+
octoStopErr := octopusContainer.Stop(ctx, &stopTime)
277325

278-
octoName, err := octopusContainer.Container.Name(ctx)
279-
if err != nil {
280-
return err
281-
}
326+
if octoStopErr != nil {
327+
t.Log("Failed to stop the Octopus container")
328+
}
282329

283-
t.Log("Octopus IP: " + octoIp)
284-
t.Log("Octopus Container Name: " + octoName)
330+
octoTerminateErr := octopusContainer.Terminate(ctx)
285331

286-
// Clean up the container after the test is complete
287-
defer func() {
288-
// This fixes the "can not get logs from container which is dead or marked for removal" error
289-
// See https://github.com/testcontainers/testcontainers-go/issues/606
290-
if os.Getenv("OCTODISABLEOCTOCONTAINERLOGGING") != "true" {
291-
stopProducerErr := octopusContainer.StopLogProducer()
292-
293-
// try to continue on if there was an error stopping the producer
294-
if stopProducerErr != nil {
295-
t.Log(stopProducerErr)
332+
if octoTerminateErr != nil {
333+
t.Log("Failed to terminate the Octopus container")
296334
}
297335
}
298336

299-
// Stop the containers
300-
stopTime := 1 * time.Minute
301-
octoStopErr := octopusContainer.Stop(ctx, &stopTime)
337+
if sqlServer != nil {
338+
sqlStopErr := sqlServer.Stop(ctx, &stopTime)
302339

303-
if octoStopErr != nil {
304-
t.Log("Failed to stop the Octopus container")
305-
}
340+
if sqlStopErr != nil {
341+
t.Log("Failed to stop the MSSQL container")
342+
}
306343

307-
sqlStopErr := sqlServer.Stop(ctx, &stopTime)
344+
sqlTerminateErr := sqlServer.Terminate(ctx)
308345

309-
if sqlStopErr != nil {
310-
t.Log("Failed to stop the MSSQL container")
346+
if sqlTerminateErr != nil {
347+
t.Log("Failed to terminate the MSSQL container")
348+
}
311349
}
312350

313351
// Terminate the containers
314-
octoTerminateErr := octopusContainer.Terminate(ctx)
315-
sqlTerminateErr := sqlServer.Terminate(ctx)
316-
317-
networkErr := network.Remove(ctx)
352+
if network != nil {
353+
networkErr := network.Remove(ctx)
318354

319-
if octoTerminateErr != nil || sqlTerminateErr != nil {
320-
t.Fatalf("failed to terminate container: %v %v", octoTerminateErr, sqlTerminateErr)
321-
}
322-
323-
if networkErr != nil {
324-
t.Fatalf("failed to remove network: %v", networkErr)
355+
if networkErr != nil {
356+
t.Log("failed to remove network: %v", networkErr)
357+
}
325358
}
326-
327-
// I've noticed some race conditions where it appears a terminated container is reused in the
328-
// retry loop. We give the Docker resources a chance to clean up before continuing.
329-
time.Sleep(30 * time.Second)
330359
}()
331360

361+
// In the event of a failed stack creation, use the defer function above
362+
// to clean up and then return the error
363+
if err != nil {
364+
return err
365+
}
366+
332367
// give the server 5 minutes to start up
333368
err = lintwait.WaitForResource(func() error {
334369
resp, err := http.Get(octopusContainer.URI + "/api")

0 commit comments

Comments
 (0)