Skip to content

Commit cdb5ca9

Browse files
committed
feat: reconciler plans the start phase
The plan learns the start vocabulary — inert until a caller opts in (ReconcileOptions.Scope, zero value keeps today's create-only plans byte-identical): - OpWaitCondition, one node per (awaited service, condition), deduplicated across dependents like networkNodes deduplicates networks; required:false marks the shared node best-effort, one required dependent upgrades it. service_started needs no node — a plain DAG edge to the dependency's chain end expresses it. Health is deliberately re-observed at execution time: the plan encodes what to wait for, never a stale observation. - OpRunPreStart, emitted at plan time only when no replica was running at observation — the imperative gating — targeting the lowest-numbered replica. - OpRunPostStart per container, after its start. - replica chains: inject+start+post_start of replica n+1 depends on the end of replica n's chain, today's sequential start order made visible in golden plans; startChainEnds points at the chain end so a service_started dependent waits for the whole service, matching InDependencyOrder semantics. - scope Start plans starting observed exited/created containers without converging them (the future compose start); scope CreateStart appends the start phase to the create plan, start nodes resolving their target from the create node that materializes the replica (CreateNodeID, the mechanism OpRenameContainer already uses). Lifecycle parity with the imperative engine is load-bearing and golden-locked: - dependency conditions are evaluated even when nothing has to start (waitDependencies runs for every visited service before looking at what to start), so an up with everything running still fails on an unhealthy required dependency; - an exceptional-state replica takes NO start-phase node: its bare create-phase restart leaves it running when the start phase looks, so the imperative engine neither re-starts nor injects — and it gates pre_start like any running replica; - startChainEnds carries the end-of-visit node set (waits included when nothing started), so a service_started dependent begins only once the dependency's whole visit completed, matching InDependencyOrder; - under scope Start, a scale>0 service with no container at all fails the plan with startService's exact error. The "service:<name>:<number>" resource-ID format is built and parsed in one place (serviceReplicaID/serviceReplicaPrefix/startGroupID), and the replica sort deliberately carries the plan's determinism over the unordered containerNodes iteration. Golden tests only; no executor support yet and no caller passes the scope. Epic #14081, Lot 1 — reconciler (first item). Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
1 parent 460cf3c commit cdb5ca9

3 files changed

Lines changed: 948 additions & 23 deletions

File tree

pkg/compose/plan.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,23 @@ const (
5353

5454
// Provider operations
5555
OpRunProvider OperationType = 30
56+
57+
// Start-phase operations
58+
OpWaitCondition OperationType = 40
59+
OpRunPreStart OperationType = 41
60+
OpRunPostStart OperationType = 42
61+
)
62+
63+
// PlanPhase situates a node in the plan lifecycle. The Create phase converges
64+
// resources and containers to their desired shape; the Start phase brings
65+
// containers to running — dependency waits, pre_start hooks, starts,
66+
// post_start hooks. The zero value is Create, so plans built before the start
67+
// phase existed render unchanged.
68+
type PlanPhase int
69+
70+
const (
71+
PhaseCreate PlanPhase = iota
72+
PhaseStart
5673
)
5774

5875
// String returns the human-readable name of an OperationType.
@@ -82,6 +99,12 @@ func (o OperationType) String() string {
8299
return "RenameContainer"
83100
case OpRunProvider:
84101
return "RunProvider"
102+
case OpWaitCondition:
103+
return "WaitCondition"
104+
case OpRunPreStart:
105+
return "RunPreStart"
106+
case OpRunPostStart:
107+
return "RunPostStart"
85108
default:
86109
return fmt.Sprintf("Unknown(%d)", int(o))
87110
}
@@ -102,7 +125,8 @@ type Operation struct {
102125
Network *types.NetworkConfig // for network operations
103126
Volume *types.VolumeConfig // for volume operations
104127
Timeout *time.Duration // for stop operations
105-
CreateNodeID int // for OpRenameContainer: ID of the CreateContainer node whose result to rename
128+
CreateNodeID int // for OpRenameContainer/start-phase ops: ID of the CreateContainer node whose result to target
129+
Condition string // for OpWaitCondition: depends_on condition to wait for (service_healthy, ...)
106130
// BestEffort marks an operation whose failure must not abort the plan. It is
107131
// used for the optional removal of the old network on a rename: if the
108132
// network is still in use (by non-Compose containers) the removal is skipped
@@ -118,6 +142,7 @@ type PlanNode struct {
118142
Operation Operation
119143
DependsOn []*PlanNode // prerequisite operations
120144
Group string // event grouping key (e.g. "recreate:web:1"); empty for ungrouped nodes
145+
Phase PlanPhase // lifecycle phase this node belongs to; zero is Create
121146
}
122147

123148
// Plan is a directed acyclic graph of operations produced by the reconciler.
@@ -171,6 +196,9 @@ func (p *Plan) String() string {
171196
if node.Group != "" {
172197
fmt.Fprintf(&sb, " [%s]", node.Group)
173198
}
199+
if node.Phase == PhaseStart {
200+
sb.WriteString(" {start}")
201+
}
174202
sb.WriteByte('\n')
175203
}
176204
return sb.String()

0 commit comments

Comments
 (0)