@@ -27,6 +27,7 @@ import (
2727 "sync"
2828
2929 "github.com/compose-spec/compose-go/v2/types"
30+ "github.com/containerd/errdefs"
3031 containerType "github.com/moby/moby/api/types/container"
3132 "github.com/moby/moby/client"
3233 "go.opentelemetry.io/otel"
@@ -43,15 +44,11 @@ import (
4344type executionState struct {
4445 mu sync.Mutex
4546 containers map [string ]Containers // service name -> containers created/updated
46- networks map [string ]string // network key -> ID
47- volumes map [string ]string // volume key -> ID
4847}
4948
5049func newExecutionState () * executionState {
5150 return & executionState {
5251 containers : make (map [string ]Containers ),
53- networks : make (map [string ]string ),
54- volumes : make (map [string ]string ),
5552 }
5653}
5754
@@ -79,18 +76,6 @@ func (es *executionState) getContainers(serviceName string) Containers {
7976 return slices .Clone (es .containers [serviceName ])
8077}
8178
82- func (es * executionState ) setNetworkID (key , id string ) {
83- es .mu .Lock ()
84- defer es .mu .Unlock ()
85- es .networks [key ] = id
86- }
87-
88- func (es * executionState ) setVolumeID (key , id string ) {
89- es .mu .Lock ()
90- defer es .mu .Unlock ()
91- es .volumes [key ] = id
92- }
93-
9479// resolveServiceReferences replaces service references in a ServiceConfig with
9580// actual container IDs from the execution state. This mirrors the logic in
9681// convergence.resolveServiceReferences but uses executionState instead.
@@ -157,6 +142,13 @@ func (s *composeService) ExecutePlan(ctx context.Context, project *types.Project
157142 return nil
158143 }
159144
145+ // Validate the plan has no dependency cycles before executing.
146+ // Without this check, a cycle would cause the executor to hang
147+ // indefinitely waiting for operations that can never be scheduled.
148+ if _ , err := topologicalSort (plan ); err != nil {
149+ return err
150+ }
151+
160152 // Pre-populate execution state with existing containers so that
161153 // resolveServiceReferences can find containers for services not
162154 // included in the plan (e.g. --no-deps scenarios).
@@ -172,7 +164,15 @@ func (s *composeService) ExecutePlan(ctx context.Context, project *types.Project
172164 expect := len (plan .Operations )
173165 eg , ctx := errgroup .WithContext (ctx )
174166 opCh := make (chan * Operation , expect )
175- defer close (opCh )
167+
168+ // sendDone sends a completed operation to the consumer goroutine,
169+ // respecting context cancellation to avoid blocking or panicking.
170+ sendDone := func (op * Operation ) {
171+ select {
172+ case opCh <- op :
173+ case <- ctx .Done ():
174+ }
175+ }
176176
177177 // Consumer goroutine: waits for completed ops and enqueues newly-ready dependents
178178 eg .Go (func () error {
@@ -195,7 +195,7 @@ func (s *composeService) ExecutePlan(ctx context.Context, project *types.Project
195195 if err := s .executeOperation (ctx , project , depOp , state ); err != nil {
196196 return err
197197 }
198- opCh <- depOp
198+ sendDone ( depOp )
199199 return nil
200200 })
201201 }
@@ -210,7 +210,7 @@ func (s *composeService) ExecutePlan(ctx context.Context, project *types.Project
210210 if err := s .executeOperation (ctx , project , op , state ); err != nil {
211211 return err
212212 }
213- opCh <- op
213+ sendDone ( op )
214214 return nil
215215 })
216216 }
@@ -245,15 +245,15 @@ func (s *composeService) executeOperation(ctx context.Context, project *types.Pr
245245func (s * composeService ) dispatchOperation (ctx context.Context , project * types.Project , op * Operation , state * executionState ) error {
246246 switch op .Type {
247247 case OpCreateNetwork :
248- return s .executePlanCreateNetwork (ctx , project , op , state )
248+ return s .executePlanCreateNetwork (ctx , project , op )
249249 case OpRemoveNetwork :
250250 return s .executePlanRemoveNetwork (ctx , project , op )
251251 case OpDisconnectNetwork :
252252 return s .executePlanDisconnectNetwork (ctx , op )
253253 case OpConnectNetwork :
254254 return s .executePlanConnectNetwork (ctx , op )
255255 case OpCreateVolume :
256- return s .executePlanCreateVolume (ctx , project , op , state )
256+ return s .executePlanCreateVolume (ctx , project , op )
257257 case OpRemoveVolume :
258258 return s .executePlanRemoveVolume (ctx , op )
259259 case OpCreateContainer :
@@ -273,13 +273,9 @@ func (s *composeService) dispatchOperation(ctx context.Context, project *types.P
273273 }
274274}
275275
276- func (s * composeService ) executePlanCreateNetwork (ctx context.Context , project * types.Project , op * Operation , state * executionState ) error {
277- id , err := s .ensureNetwork (ctx , project , op .NetworkOp .NetworkKey , op .NetworkOp .Desired )
278- if err != nil {
279- return err
280- }
281- state .setNetworkID (op .NetworkOp .NetworkKey , id )
282- return nil
276+ func (s * composeService ) executePlanCreateNetwork (ctx context.Context , project * types.Project , op * Operation ) error {
277+ _ , err := s .ensureNetwork (ctx , project , op .NetworkOp .NetworkKey , op .NetworkOp .Desired )
278+ return err
283279}
284280
285281func (s * composeService ) executePlanRemoveNetwork (ctx context.Context , project * types.Project , op * Operation ) error {
@@ -301,17 +297,13 @@ func (s *composeService) executePlanConnectNetwork(ctx context.Context, op *Oper
301297 return err
302298}
303299
304- func (s * composeService ) executePlanCreateVolume (ctx context.Context , project * types.Project , op * Operation , state * executionState ) error {
300+ func (s * composeService ) executePlanCreateVolume (ctx context.Context , project * types.Project , op * Operation ) error {
305301 volume := * op .VolumeOp .Desired
306302 volume .CustomLabels = volume .CustomLabels .Add (api .VolumeLabel , op .VolumeOp .VolumeKey )
307303 volume .CustomLabels = volume .CustomLabels .Add (api .ProjectLabel , project .Name )
308304 volume .CustomLabels = volume .CustomLabels .Add (api .VersionLabel , api .ComposeVersion )
309- id , err := s .ensureVolume (ctx , op .VolumeOp .VolumeKey , volume , project )
310- if err != nil {
311- return err
312- }
313- state .setVolumeID (op .VolumeOp .VolumeKey , id )
314- return nil
305+ _ , err := s .ensureVolume (ctx , op .VolumeOp .VolumeKey , volume , project )
306+ return err
315307}
316308
317309func (s * composeService ) executePlanRemoveVolume (ctx context.Context , op * Operation ) error {
@@ -410,8 +402,18 @@ func (s *composeService) executePlanStopContainer(ctx context.Context, op *Opera
410402}
411403
412404func (s * composeService ) executePlanRemoveContainer (ctx context.Context , op * Operation ) error {
413- service := op .ContainerOp .Service
414- return s .stopAndRemoveContainer (ctx , * op .ContainerOp .Existing , & service , op .ContainerOp .Timeout , false )
405+ ctr := * op .ContainerOp .Existing
406+ eventName := getContainerProgressName (ctr )
407+ s .events .On (removingEvent (eventName ))
408+ _ , err := s .apiClient ().ContainerRemove (ctx , ctr .ID , client.ContainerRemoveOptions {
409+ Force : true ,
410+ })
411+ if err != nil && ! errdefs .IsNotFound (err ) && ! errdefs .IsConflict (err ) {
412+ s .events .On (errorEvent (eventName , "Error while Removing" ))
413+ return err
414+ }
415+ s .events .On (removedEvent (eventName ))
416+ return nil
415417}
416418
417419func (s * composeService ) executePlanRunPlugin (ctx context.Context , project * types.Project , op * Operation ) error {
0 commit comments