executor: pre_start re-checks the daemon before running

The 'once per service, only when no replica is running' rule is
decided at plan time from the observed state — the placement of the
RunPreStart node stays a plan decision. But between observation and
execution a replica can start (the general observe-to-execute drift
window the plan design accepts): for pre_start that drift would re-run
hooks the rule says must not run. The operation now re-checks the
daemon at execution time and skips itself when a replica is already
running — plan-time placement, runtime guard.

Validated design decision for #14081 (start-candidates model).

Signed-off-by: Nicolas De Loof <[email protected]>
This commit is contained in:
Nicolas De Loof
2026-08-17 21:46:06 +02:00
parent 371dd6f744
commit 05258d85fa
+13
View File
@@ -279,6 +279,19 @@ func (exec *planExecutor) checkWaitCondition(ctx context.Context, op Operation,
// execRunPreStart runs the service's pre_start hooks on the replica the plan
// selected (once per service, before any of its containers start).
func (exec *planExecutor) execRunPreStart(ctx context.Context, op Operation) error {
// The plan scheduled this node because no replica was running at
// observation time. Re-check against the daemon at execution time: if a
// replica started in the meantime (observe-to-execute drift), the
// once-per-service rule says the hooks must not run again.
running, err := exec.compose.getContainers(ctx, exec.project.Name, oneOffExclude, false, op.Service.Name)
if err != nil {
return err
}
if len(running) > 0 {
logrus.Debugf("skipping pre_start hooks of service %s: a replica is already running", op.Service.Name)
return nil
}
id, name := exec.resolveContainer(op)
if id == "" {
return fmt.Errorf("no container to run pre_start hooks for %s", op.ResourceID)