@@ -16,6 +16,7 @@ import (
1616 "github.com/containerd/errdefs"
1717 "github.com/moby/moby/api/types/container"
1818 "github.com/moby/moby/api/types/network"
19+ "github.com/moby/moby/client"
1920 "github.com/stretchr/testify/require"
2021
2122 "github.com/testcontainers/testcontainers-go/internal/config"
@@ -439,6 +440,73 @@ func Test_RecreateReaperIfTerminated(t *testing.T) {
439440 require .NoError (t , err , "connecting to Reaper should be successful" )
440441}
441442
443+ // Test_RecreateReaperIfStopped tests that a reaper container which still exists
444+ // but is no longer running, e.g. it shut down after its reconnection timeout
445+ // with no clients but was not removed yet, is replaced instead of being waited
446+ // on until the startup timeout expires.
447+ func Test_RecreateReaperIfStopped (t * testing.T ) {
448+ reaperDisable (t , false )
449+
450+ SkipIfProviderIsNotHealthy (t )
451+
452+ ctx := context .Background ()
453+
454+ provider , err := NewDockerProvider ()
455+ require .NoError (t , err )
456+
457+ // Create a stopped container that the lookup identifies as the session's
458+ // reaper: same name and labels, but exited and not auto-removed.
459+ require .NoError (t , provider .PullImage (ctx , alpineImage ))
460+
461+ labels := core .DefaultLabels (testSessionID )
462+ labels [core .LabelReaper ] = "true"
463+ labels [core .LabelRyuk ] = "true"
464+ delete (labels , core .LabelReap )
465+
466+ cli := provider .Client ()
467+ created , err := cli .ContainerCreate (ctx , client.ContainerCreateOptions {
468+ Config : & container.Config {
469+ Image : alpineImage ,
470+ Cmd : []string {"true" },
471+ Labels : labels ,
472+ },
473+ Name : reaperContainerNameFromSessionID (testSessionID ),
474+ })
475+ require .NoError (t , err )
476+ t .Cleanup (func () {
477+ if _ , err := cli .ContainerRemove (context .Background (), created .ID , client.ContainerRemoveOptions {Force : true }); err != nil && ! errdefs .IsNotFound (err ) {
478+ require .NoError (t , err )
479+ }
480+ })
481+
482+ _ , err = cli .ContainerStart (ctx , created .ID , client.ContainerStartOptions {})
483+ require .NoError (t , err )
484+
485+ require .Eventually (t , func () bool {
486+ inspect , err := cli .ContainerInspect (ctx , created .ID , client.ContainerInspectOptions {})
487+ return err == nil && ! inspect .Container .State .Running
488+ }, time .Second * 10 , time .Millisecond * 100 , "stopped reaper container should have exited" )
489+
490+ // The stopped container must be replaced by a fresh reaper well within
491+ // the startup timeout that waiting on it for readiness would burn.
492+ timeout , cancel := context .WithTimeout (ctx , time .Second * 30 )
493+ defer cancel ()
494+
495+ spawner := & reaperSpawner {}
496+ reaper , err := spawner .reaper (context .WithValue (timeout , core .DockerHostContextKey , provider .host ), testSessionID , provider )
497+ cleanupReaper (t , reaper , spawner )
498+ require .NoError (t , err , "creating the Reaper should not error" )
499+ require .NotEqual (t , created .ID , reaper .container .GetContainerID (), "expected a new reaper container" )
500+
501+ // The stopped container was removed to free up the reaper name.
502+ _ , err = cli .ContainerInspect (ctx , created .ID , client.ContainerInspectOptions {})
503+ require .True (t , errdefs .IsNotFound (err ), "stopped reaper container should have been removed, got: %v" , err )
504+
505+ termSignal , err := reaper .Connect ()
506+ cleanupTermSignal (t , termSignal )
507+ require .NoError (t , err , "connecting to Reaper should be successful" )
508+ }
509+
442510func TestReaper_reuseItFromOtherTestProgramUsingDocker (t * testing.T ) {
443511 reaperDisable (t , false )
444512
0 commit comments