feat(executor): inner steps inherit the timeout of the step that uses the action
- commit
- 5f78aaa9e2c66f185392cb5554f5d9f92f7b004c
- author
- brice · · 2026-10-14 03:27 UTC
- parent
- 3ee1c67
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +25 -1
M internal/executor/step.go +5 -0
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "context" | |
| 7 | 7 | "errors" | |
| 8 | 8 | "strings" | |
| 9 | + | "time" | |
| 9 | 10 | ||
| 10 | 11 | "bnema.dev/ermite-actions/action" | |
| 11 | 12 | "bnema.dev/ermite-actions/expr" | |
| @@ -50,6 +51,10 @@ type stepRun struct { | |||
| 50 | 51 | scope stepScope | |
| 51 | 52 | name string | |
| 52 | 53 | timedOut timedOutFlag | |
| 54 | + | // deadline is when the timeout-minutes of the step, or of the step around it, | |
| 55 | + | // ends: zero for none. An inner step of a composite action inherits what is | |
| 56 | + | // left of it (ExecutionContext.CreateEmbeddedChild, :498). | |
| 57 | + | deadline time.Time | |
| 53 | 58 | } | |
| 54 | 59 | ||
| 55 | 60 | // depth is the number of composite actions around the step. | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "context" | 6 | "context" | ||
| 7 | "errors" | 7 | "errors" | ||
| 8 | "strings" | 8 | "strings" | ||
| 9 | + | "time" | |||
| 9 | 10 | ||||
| 10 | "bnema.dev/ermite-actions/action" | 11 | "bnema.dev/ermite-actions/action" | ||
| 11 | "bnema.dev/ermite-actions/expr" | 12 | "bnema.dev/ermite-actions/expr" | ||
| @@ -50,6 +51,10 @@ type stepRun struct { | |||||
| 50 | scope stepScope | 51 | scope stepScope | ||
| 51 | name string | 52 | name string | ||
| 52 | timedOut timedOutFlag | 53 | timedOut timedOutFlag | ||
| 54 | + | // deadline is when the timeout-minutes of the step, or of the step around it, | |||
| 55 | + | // ends: zero for none. An inner step of a composite action inherits what is | |||
| 56 | + | // left of it (ExecutionContext.CreateEmbeddedChild, :498). | |||
| 57 | + | deadline time.Time | |||
| 53 | } | 58 | } | ||
| 54 | 59 | ||||
| 55 | // depth is the number of composite actions around the step. | 60 | // depth is the number of composite actions around the step. | ||
M internal/executor/step_run.go +20 -1
| @@ -5,6 +5,7 @@ package executor | |||
| 5 | 5 | import ( | |
| 6 | 6 | "context" | |
| 7 | 7 | "sync" | |
| 8 | + | "time" | |
| 8 | 9 | ||
| 9 | 10 | "bnema.dev/ermite-actions/action" | |
| 10 | 11 | "bnema.dev/ermite-actions/expr" | |
| @@ -303,6 +304,16 @@ func (j *job) runGuarded(s *stepRun, q qstep, ok bool) stepResult { | |||
| 303 | 304 | stopTimeout, minutes := func() (func(), int) { return func() {}, 0 }() | |
| 304 | 305 | if q.setup == nil { | |
| 305 | 306 | stopTimeout, minutes = j.armStepTimeout(s, closeCancel) | |
| 307 | + | if minutes > 0 { | |
| 308 | + | s.deadline = j.now().Add(time.Duration(minutes)*time.Minute + j.cfg.StepTimeoutSkew) | |
| 309 | + | } | |
| 310 | + | if p := s.parent; p != nil && !p.deadline.IsZero() { | |
| 311 | + | // The inner step has the time that is left to the step around it, and | |
| 312 | + | // no timeout of its own (the schema refuses one). | |
| 313 | + | s.deadline = p.deadline | |
| 314 | + | t := time.AfterFunc(time.Until(p.deadline), func() { s.timedOut.Store(true); closeCancel() }) | |
| 315 | + | stopTimeout = func() { t.Stop() } | |
| 316 | + | } | |
| 306 | 317 | } | |
| 307 | 318 | if q.setup != nil { | |
| 308 | 319 | r = q.setup(s, stepCancel) | |
| @@ -312,7 +323,15 @@ func (j *job) runGuarded(s *stepRun, q qstep, ok bool) stepResult { | |||
| 312 | 323 | stopTimeout() | |
| 313 | 324 | close(stopWatch) | |
| 314 | 325 | watch.Wait() | |
| 315 | - | r = j.stepTimedOut(s, r, minutes) | |
| 326 | + | if minutes > 0 { | |
| 327 | + | r = j.stepTimedOut(s, r, minutes) | |
| 328 | + | } | |
| 329 | + | if s.parent != nil && r == resCanceled && s.timedOut.Load() && !j.cancelRequested() { | |
| 330 | + | // CompositeActionHandler.RunStepAsync (:497-506): an inner step stopped by | |
| 331 | + | // the timeout it inherited fails with a message that names no step. | |
| 332 | + | j.errorf(num, "The action has timed out.") | |
| 333 | + | r = resFailed | |
| 334 | + | } | |
| 316 | 335 | ||
| 317 | 336 | // Merge the result of the commands (StepsRunner.cs:347-351). | |
| 318 | 337 | if s.cmdResult != nil { | |
| @@ -5,6 +5,7 @@ package executor | |||||
| 5 | import ( | 5 | import ( | ||
| 6 | "context" | 6 | "context" | ||
| 7 | "sync" | 7 | "sync" | ||
| 8 | + | "time" | |||
| 8 | 9 | ||||
| 9 | "bnema.dev/ermite-actions/action" | 10 | "bnema.dev/ermite-actions/action" | ||
| 10 | "bnema.dev/ermite-actions/expr" | 11 | "bnema.dev/ermite-actions/expr" | ||
| @@ -303,6 +304,16 @@ func (j *job) runGuarded(s *stepRun, q qstep, ok bool) stepResult { | |||||
| 303 | stopTimeout, minutes := func() (func(), int) { return func() {}, 0 }() | 304 | stopTimeout, minutes := func() (func(), int) { return func() {}, 0 }() | ||
| 304 | if q.setup == nil { | 305 | if q.setup == nil { | ||
| 305 | stopTimeout, minutes = j.armStepTimeout(s, closeCancel) | 306 | stopTimeout, minutes = j.armStepTimeout(s, closeCancel) | ||
| 307 | + | if minutes > 0 { | |||
| 308 | + | s.deadline = j.now().Add(time.Duration(minutes)*time.Minute + j.cfg.StepTimeoutSkew) | |||
| 309 | + | } | |||
| 310 | + | if p := s.parent; p != nil && !p.deadline.IsZero() { | |||
| 311 | + | // The inner step has the time that is left to the step around it, and | |||
| 312 | + | // no timeout of its own (the schema refuses one). | |||
| 313 | + | s.deadline = p.deadline | |||
| 314 | + | t := time.AfterFunc(time.Until(p.deadline), func() { s.timedOut.Store(true); closeCancel() }) | |||
| 315 | + | stopTimeout = func() { t.Stop() } | |||
| 316 | + | } | |||
| 306 | } | 317 | } | ||
| 307 | if q.setup != nil { | 318 | if q.setup != nil { | ||
| 308 | r = q.setup(s, stepCancel) | 319 | r = q.setup(s, stepCancel) | ||
| @@ -312,7 +323,15 @@ func (j *job) runGuarded(s *stepRun, q qstep, ok bool) stepResult { | |||||
| 312 | stopTimeout() | 323 | stopTimeout() | ||
| 313 | close(stopWatch) | 324 | close(stopWatch) | ||
| 314 | watch.Wait() | 325 | watch.Wait() | ||
| 315 | - | r = j.stepTimedOut(s, r, minutes) | 326 | + | if minutes > 0 { |
| 327 | + | r = j.stepTimedOut(s, r, minutes) | |||
| 328 | + | } | |||
| 329 | + | if s.parent != nil && r == resCanceled && s.timedOut.Load() && !j.cancelRequested() { | |||
| 330 | + | // CompositeActionHandler.RunStepAsync (:497-506): an inner step stopped by | |||
| 331 | + | // the timeout it inherited fails with a message that names no step. | |||
| 332 | + | j.errorf(num, "The action has timed out.") | |||
| 333 | + | r = resFailed | |||
| 334 | + | } | |||
| 316 | 335 | ||||
| 317 | // Merge the result of the commands (StepsRunner.cs:347-351). | 336 | // Merge the result of the commands (StepsRunner.cs:347-351). | ||
| 318 | if s.cmdResult != nil { | 337 | if s.cmdResult != nil { | ||