diff --git a/pkg/compose/executor_ops.go b/pkg/compose/executor_ops.go index 47192c2d9..2231cd350 100644 --- a/pkg/compose/executor_ops.go +++ b/pkg/compose/executor_ops.go @@ -265,6 +265,15 @@ func (exec *planExecutor) execWaitCondition(ctx context.Context, op Operation) e // accepts — skips a second, redundant run of the hooks rather than erroring // on runners already consumed. func (exec *planExecutor) execRunPreStart(ctx context.Context, op Operation) error { + // Same interim narrowing as execStartContainer's guard (see its + // comment), not a full fix: this node hangs off the replica's create + // node, whose done-channel closes before errgroup's ctx is canceled when + // that create fails for a genuine reason, so a narrow window remains + // where pre_start hooks could still run against the runner containers. + if err := ctx.Err(); err != nil { + return err + } + running, err := exec.compose.getContainers(ctx, exec.project.Name, oneOffExclude, false, op.Service.Name) if err != nil { return err diff --git a/pkg/compose/executor_start_test.go b/pkg/compose/executor_start_test.go index 2bd37ae3b..3f89f59d7 100644 --- a/pkg/compose/executor_start_test.go +++ b/pkg/compose/executor_start_test.go @@ -211,6 +211,26 @@ func TestExecRunPreStart_SkipsWhenReplicaAlreadyRunning(t *testing.T) { assert.NilError(t, err) } +// TestExecRunPreStart_BailsOutWhenContextCanceled pins the same interim +// narrowing execStartContainer and execRunPostStart carry: once ctx is +// canceled (a sibling node failed), the node must neither touch the daemon +// nor run hooks. No ContainerList expectation is registered: an unexpected +// call fails the test. +func TestExecRunPreStart_BailsOutWhenContextCanceled(t *testing.T) { + svc, _, _ := newStartPhaseTestService(t) + + service := types.ServiceConfig{ + Name: "web", + PreStart: []types.PreStartHook{{ContainerSpec: types.ContainerSpec{Command: types.ShellCommand{"init"}}}}, + } + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + exec := svc.newPlanExecutor(&types.Project{Name: "test"}, emptyObservedState("test"), nil) + err := exec.execRunPreStart(ctx, Operation{Service: &service}) + assert.ErrorIs(t, err, context.Canceled) +} + // TestExecRunPostStart_RunsHookAndStreamsToListener verifies that // execRunPostStart resolves the replica the start chain just brought up and // runs its post_start hooks, forwarding their output to the listener the