@@ -22,6 +22,7 @@ import (
2222 "fmt"
2323 "reflect"
2424 "strings"
25+ "sync"
2526 "testing"
2627 "time"
2728
@@ -646,3 +647,133 @@ func (m *mockEventHandler) GetEvents(request *events.Request) ([]*info.Event, er
646647
647648func (m * mockEventHandler ) StopWatch (watchID int ) {
648649}
650+
651+ // threadSafeEventHandler is a thread-safe version of mockEventHandler for concurrent tests.
652+ type threadSafeEventHandler struct {
653+ mu sync.Mutex
654+ events []* info.Event
655+ }
656+
657+ func (m * threadSafeEventHandler ) AddEvent (e * info.Event ) error {
658+ m .mu .Lock ()
659+ defer m .mu .Unlock ()
660+ m .events = append (m .events , e )
661+ return nil
662+ }
663+
664+ func (m * threadSafeEventHandler ) WatchEvents (request * events.Request ) (* events.EventChannel , error ) {
665+ return nil , fmt .Errorf ("not implemented" )
666+ }
667+
668+ func (m * threadSafeEventHandler ) GetEvents (request * events.Request ) ([]* info.Event , error ) {
669+ return nil , fmt .Errorf ("not implemented" )
670+ }
671+
672+ func (m * threadSafeEventHandler ) StopWatch (watchID int ) {
673+ }
674+
675+ // TestContainerDataStopConcurrent verifies that concurrent calls to Stop()
676+ // do not cause a panic. This is a regression test for a race condition where
677+ // multiple goroutines could call Stop() on the same containerData, causing a
678+ // "close of closed channel" panic.
679+ func TestContainerDataStopConcurrent (t * testing.T ) {
680+ memoryCache := memory .New (60 * time .Second , nil )
681+
682+ // Create a minimal containerData with the fields needed for Stop()
683+ cd := & containerData {
684+ info : containerInfo {
685+ ContainerReference : info.ContainerReference {
686+ Name : "/test-concurrent" ,
687+ },
688+ },
689+ memoryCache : memoryCache ,
690+ stop : make (chan struct {}),
691+ perfCollector : & stats.NoopCollector {},
692+ resctrlCollector : & stats.NoopCollector {},
693+ }
694+
695+ // Launch multiple goroutines that all try to call Stop() simultaneously
696+ const numGoroutines = 10
697+ var wg sync.WaitGroup
698+ wg .Add (numGoroutines )
699+
700+ // Use a channel to synchronize goroutines to start at the same time
701+ start := make (chan struct {})
702+
703+ for i := 0 ; i < numGoroutines ; i ++ {
704+ go func () {
705+ defer wg .Done ()
706+ <- start // Wait for signal to start
707+ // This should not panic even if called multiple times concurrently
708+ _ = cd .Stop ()
709+ }()
710+ }
711+
712+ // Signal all goroutines to start simultaneously
713+ close (start )
714+
715+ // Wait for all goroutines to complete - if there's a panic, the test will fail
716+ wg .Wait ()
717+ }
718+
719+ // TestDestroyContainerConcurrent verifies that concurrent calls to destroyContainer
720+ // for the same container do not cause a panic.
721+ func TestDestroyContainerConcurrent (t * testing.T ) {
722+ memoryCache := memory .New (60 * time .Second , nil )
723+ m := & manager {
724+ quitChannels : make ([]chan error , 0 , 2 ),
725+ memoryCache : memoryCache ,
726+ }
727+
728+ mockEventHandler := & threadSafeEventHandler {}
729+ mockHandler := containertest .NewMockContainerHandler ("/test-concurrent" )
730+ mockHandler .On ("Start" ).Return (nil )
731+ mockHandler .On ("Cleanup" ).Return ()
732+ mockHandler .On ("Stop" ).Return ()
733+ // GetExitCode may be called multiple times due to concurrent access
734+ mockHandler .On ("GetExitCode" ).Return (- 1 , nil ).Maybe ()
735+
736+ // Create the container
737+ cd := & containerData {
738+ handler : mockHandler ,
739+ info : containerInfo {
740+ ContainerReference : info.ContainerReference {
741+ Name : "/test-concurrent" ,
742+ },
743+ },
744+ memoryCache : memoryCache ,
745+ stop : make (chan struct {}),
746+ perfCollector : & stats.NoopCollector {},
747+ resctrlCollector : & stats.NoopCollector {},
748+ }
749+
750+ // Add to manager's container map
751+ m .containers .Store (namespacedContainerName {Name : "/test-concurrent" }, cd )
752+
753+ // Register event handler
754+ m .eventHandler = mockEventHandler
755+
756+ // Launch multiple goroutines that all try to destroy the same container
757+ const numGoroutines = 10
758+ var wg sync.WaitGroup
759+ wg .Add (numGoroutines )
760+
761+ start := make (chan struct {})
762+
763+ for i := 0 ; i < numGoroutines ; i ++ {
764+ go func () {
765+ defer wg .Done ()
766+ <- start
767+ _ = m .destroyContainer ("/test-concurrent" )
768+ }()
769+ }
770+
771+ close (start )
772+ wg .Wait ()
773+
774+ // With sync.Once protecting only the channel close, multiple goroutines
775+ // can still process the container and add events. The key assertion is
776+ // that no panic occurred (test would fail otherwise).
777+ // At least one event should be recorded.
778+ assert .GreaterOrEqual (t , len (mockEventHandler .events ), 1 , "at least one destruction event should be recorded" )
779+ }
0 commit comments