Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions pkg/compose/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,10 @@ type planExecutor struct {
project *types.Project
pctx *reconciliationContext

// listener streams pre_start/post_start hook logs, exactly like the
// imperative start path. It is set once, when the Start phase is entered
// (planRun.runStart): an attached caller only has one by then, its printer
// not existing before the Create phase is done. nil leaves hook execution
// silent, matching detached behavior.
// listener streams pre_start/post_start hook logs. It is set once, when
// the Start phase is entered (planRun.runStart): an attached caller only
// has one by then, its printer not existing before the Create phase is
// done. nil leaves hook execution silent, matching detached behavior.
listener api.ContainerEventListener

// containersByService is a live view used to resolve service references
Expand Down Expand Up @@ -379,8 +378,9 @@ func (p *phaseCancel) fail(node *PlanNode, events api.EventProcessor, groups *gr
// latter needs a dependency that is already running, hence with no start
// node for the wait to depend on, whose health relies on a sibling the plan
// has yet to start: with a bounded --parallel the wait could take the slot
// that sibling's start needs and only end at its timeout. The imperative
// waitDependencies stays outside the cap for the same reason.
// that sibling's start needs and only end at its timeout. The waits
// waitDependencies runs outside any plan stay outside the cap for the same
// reason.
func acquireNodeSlot(ctx context.Context, limiter *semaphore.Weighted, node *PlanNode) (func(), error) {
if node.Operation.Type == OpWaitCondition {
return func() {}, nil
Expand Down Expand Up @@ -550,9 +550,9 @@ func (e *waitInterrupted) Error() string { return "wait interrupted" }

// translateWaitTimeout turns a plan failure caused by a wait node's own
// --wait-timeout window (see errWaitTimeout) into the "timeout waiting for
// dependencies" message the imperative start path always reported for a
// depends_on wait timing out, with or without --wait, and leaves any other
// error as is. It is deliberately not the "application not healthy" message:
// dependencies" message a depends_on wait timing out has always reported,
// with or without --wait, and leaves any other error as is. It is
// deliberately not the "application not healthy" message:
// that one belongs to the final --wait readiness check that runs after the
// plan, which has its own translation.
func translateWaitTimeout(err error) error {
Expand Down
5 changes: 2 additions & 3 deletions pkg/compose/executor_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,8 @@ func (gt *groupTracker) onNodeStart(node *PlanNode, events api.EventProcessor) b
// triggersWorking reports whether this node starting should fire the group's
// Working event. A recreate group fires on its first node; a start group
// fires specifically on OpStartContainer — pre_start hooks, when planned,
// run silently before it, matching the imperative engine's startService,
// which never surfaces a Starting event until the ContainerStart call itself
// begins.
// run silently before it: no Starting event is surfaced until the
// ContainerStart call itself begins.
func (gs *groupState) triggersWorking(t OperationType) bool {
if gs.kind == groupStart {
return t == OpStartContainer
Expand Down
8 changes: 4 additions & 4 deletions pkg/compose/executor_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func (exec *planExecutor) execCreateContainer(ctx context.Context, node *PlanNod
// today only the create phase's exceptional-state restart of a paused/dead
// container) is a plain ContainerStart. A start-phase operation (Service set)
// performs the full service start — secret/config injection right before
// ContainerStart — mirroring startServiceContainer; its target resolves
// ContainerStart; its target resolves
// either from the observed container or, for a replica the plan itself
// creates, from the CreateContainer node's result (the same mechanism
// OpRenameContainer already uses).
Expand Down Expand Up @@ -213,9 +213,9 @@ func (exec *planExecutor) resolveContainerSummary(op Operation) (container.Summa
// execWaitCondition polls the dependency service named by the operation
// until it satisfies the declared depends_on condition or the context ends —
// the plan-side equivalent of one waitDependencies edge, reusing the exact
// same polling primitive (waitDependency) and per-condition checks the
// imperative engine uses, so events and error messages cannot drift between
// the two. required: false dependencies are marked BestEffort by the
// same polling primitive (waitDependency) and per-condition checks as
// waitDependencies, so events and error messages cannot drift between the
// two. required: false dependencies are marked BestEffort by the
// reconciler: a missing dependency or a failed/timed-out condition is then a
// warning, not a plan failure — matching waitDependencies' own
// optional-dependency handling.
Expand Down
9 changes: 3 additions & 6 deletions pkg/compose/executor_start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func TestExecWaitCondition_OptionalMissingDependencyIsTolerated(t *testing.T) {
// TestExecWaitCondition_HealthyConditionSatisfied drives execWaitCondition
// against a live dependency container that reports healthy on the first
// probe, reusing the exact same isServiceHealthy/waitDependency primitives
// the imperative engine's waitDependencies uses.
// waitDependencies uses.
func TestExecWaitCondition_HealthyConditionSatisfied(t *testing.T) {
svc, apiClient, recorder := newStartPhaseTestService(t)

Expand Down Expand Up @@ -154,9 +154,7 @@ func TestExecWaitCondition_HealthyConditionSatisfied(t *testing.T) {

// TestExecutePlanWaitConditionRespectsContextDeadline is a regression test
// for a gap a reviewer caught on upDetached (pkg/compose/up.go): the plan's
// own OpWaitCondition nodes have no timeout of their own (unlike the
// imperative engine's waitDependencies, which already threaded WaitTimeout
// through every per-dependency wait, not just the final --wait check) --
// own OpWaitCondition nodes have no timeout of their own --
// they rely entirely on whatever ctx they're given (directly, or via
// exec.waitTimeout -- see TestExecutePlanWaitDeadlineOnlyBoundsWaitNodes for
// that path specifically). This drives a real OpWaitCondition node
Expand Down Expand Up @@ -1020,8 +1018,7 @@ func TestExecutePlanStartPhaseEventContract(t *testing.T) {
// container.Summary — CreateNodeID only), exercising resolveContainerSummary's
// live-view lookup. It asserts the group emits exactly one Starting→Started
// pair, with Starting firing at the StartContainer node — not at
// RunPreStart, which must run silently first, matching the imperative
// engine's startService.
// RunPreStart, which must run silently first.
func TestExecutePlanStartPhaseFullChainThroughCreateNode(t *testing.T) {
svc, apiClient, recorder := newStartPhaseTestService(t)

Expand Down
2 changes: 1 addition & 1 deletion pkg/compose/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ func emptyObservedState(project string) *ObservedState {
// wiring and group tracker — not a hand-rolled loop over executeNode.
// A best-effort removal failure (stale pre_start hook runner purge) is
// warn-only: the plan carries on, and the removal passes RemoveVolumes so the
// runner's anonymous volumes go with it — the imperative purge semantics.
// runner's anonymous volumes go with it.
func TestExecutePlanBestEffortRemoveContainerFailureTolerated(t *testing.T) {
svc, apiClient := newTestService(t)

Expand Down
8 changes: 4 additions & 4 deletions pkg/compose/observed_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,8 @@ func (s *composeService) collectObservedState(ctx context.Context, project *type
// phase's own RunPreStart never reads observed.HookContainers, it
// executes a runner the create phase already prepared or fails outright
// if none exists. Skipping this for ScopeStart matches the
// network/volume guard below: one more unconditional daemon call the old
// imperative engine never made, which could otherwise fail a no-op
// network/volume guard below: one more unconditional daemon call a start
// never made, which could otherwise fail a no-op
// `compose start` on a service with pre_start hooks declared (second
// Copilot review finding in the same vein as the first).
if scope.plansCreatePhase() {
Expand Down Expand Up @@ -265,8 +265,8 @@ func (s *composeService) collectObservedState(ctx context.Context, project *type
// reconcileNetworks/reconcileVolumes, which ScopeStart never runs, do).
// Skipping these calls for ScopeStart isn't just an optimization -- a
// Copilot review finding on #14296 noted that querying them unconditionally
// gives `compose start` new daemon dependencies the old imperative engine
// never had (only ever called getContainers): an authorization plugin
// gives `compose start` new daemon dependencies it never had (it only ever
// called getContainers): an authorization plugin
// permitting container list/start but denying network or volume listing
// would now make `compose start` fail before starting anything.
if scope.plansCreatePhase() {
Expand Down
4 changes: 2 additions & 2 deletions pkg/compose/observed_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,8 @@ func TestCollectObservedState(t *testing.T) {
// test for a Copilot review finding on #14296: ScopeStart's planStartPhase
// never reads resolvedNetworks/resolvedVolumes (only reconcileNetworks/
// reconcileVolumes, which ScopeStart never runs, do), so querying them
// unconditionally gave `compose start` new daemon dependencies the old
// imperative engine never had (only ever called getContainers) -- a problem
// unconditionally gave `compose start` new daemon dependencies it never had
// (it only ever called getContainers) -- a problem
// for an authorization plugin permitting container list/start but denying
// network or volume listing. No NetworkList/VolumeList/NetworkInspect/
// VolumeInspect expectation is registered here: gomock fails the test if
Expand Down
6 changes: 3 additions & 3 deletions pkg/compose/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,14 @@ type Operation struct {
Condition string // for OpWaitCondition: depends_on condition to wait for (service_healthy, ...)
HookIndex int // for OpCreateHookContainer: position of the hook in the service's hook list
// RemoveVolumes asks OpRemoveContainer to also remove the container's
// anonymous volumes — the imperative semantics for hook-runner containers.
// anonymous volumes — what removing a hook-runner container has always done.
RemoveVolumes bool
// BestEffort marks an operation whose failure must not abort the plan.
// Used for the optional removal of the old network on a rename (if the
// network is still in use by non-Compose containers the removal is skipped
// with a warning — the new network already carries a different name), and
// for purging stale pre_start hook runners (the imperative purge is
// warn-only: a failed removal leaves the container visible, never blocks).
// for purging stale pre_start hook runners (a failed removal only warns:
// it leaves the container visible, never blocks).
BestEffort bool
}

Expand Down
Loading
Loading