feat(spec): add init handler field#1455
Conversation
WalkthroughAdds an Init lifecycle handler that runs after preconditions and before workflow steps; Init failure prevents steps from running. Introduces a canonical Abort handler (deprecating Cancel), adds skip-base-handlers support for sub-DAGs, and wires Init/Abort through DAG model, spec, loader, builder, runtime (agent/runner/transform), schema, CLI, and integration tests. Changes
Sequence DiagramsequenceDiagram
participant User as User/Executor
participant Agent as Agent
participant Runner as Runner
participant Handlers as Handlers
participant Steps as Workflow Steps
User->>Agent: Start DAG run
Agent->>Agent: Parse DAG spec (Init, Abort, etc.)
Agent->>Runner: newRunner(cfg with OnInit)
Runner->>Runner: Evaluate preconditions
alt Preconditions pass
Runner->>Handlers: Execute OnInit (if defined)
alt OnInit succeeds
Runner->>Steps: Execute workflow steps
alt Steps succeed
Runner->>Handlers: Execute OnSuccess
Runner->>Handlers: Execute OnExit
Runner->>Agent: Report Succeeded
else Steps fail
Runner->>Handlers: Execute OnFailure
Runner->>Handlers: Execute OnExit
Runner->>Agent: Report Failed
end
else OnInit fails
Runner->>Handlers: Execute OnExit
Runner->>Agent: Report Aborted (Init failure)
end
else Preconditions fail
Runner->>Agent: Report Aborted (preconditions)
end
alt Manual abort during run
User->>Runner: Abort()
Runner->>Handlers: Execute OnAbort/OnCancel (mapped to Abort)
Runner->>Handlers: Execute OnExit
Runner->>Agent: Report Aborted
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used📓 Path-based instructions (2)**/*.go📄 CodeRabbit inference engine (AGENTS.md)
Files:
**/*_test.go📄 CodeRabbit inference engine (AGENTS.md)
Files:
🧠 Learnings (1)📚 Learning: 2025-12-04T10:34:17.051ZApplied to files:
🧬 Code graph analysis (1)internal/integration/handler_on_test.go (3)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
🔇 Additional comments (2)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (4)
schemas/dag.schema.json (1)
172-198: Schema additions forhandlerOn.init/abort/cancelalign with runtime behaviorThe new
initandabortproperties and the updatedcanceldescription accurately describe how the runtime treats these hooks (init before steps; abort/cancel on aborted DAG; cancel deprecated). If you expect users to rely heavily on init preconditions, consider later clarifying in the description whether “If this fails” refers specifically to the init command failing vs. its preconditions being unmet, but this is not a blocker.internal/core/spec/builder.go (1)
724-778: HandlerOn Init and Abort/Cancel build logic is sound; consider warning on deprecatedcancelusageThe Init handler is built consistently with the other hooks, and the Abort/Cancel logic (prefer
abort, error if both are set, map the chosen one intodag.HandlerOn.Cancelwith theonCancelname) cleanly implements the “Abort canonical / Cancel deprecated alias” story.As a small UX improvement, you might want to surface a deprecation warning when
handlerOn.cancelis used withouthandlerOn.abort, similar to howrunvscallis handled elsewhere, e.g.:@@ func buildHandlers(ctx BuildContext, spec *definition, dag *core.DAG) (err error) { - var abortDef *stepDef + var abortDef *stepDef switch { case spec.HandlerOn.Abort != nil: abortDef = spec.HandlerOn.Abort case spec.HandlerOn.Cancel != nil: + // handlerOn.cancel is deprecated; prefer handlerOn.abort + message := "handlerOn.cancel is deprecated, use handlerOn.abort instead" + logger.Warn(ctx.ctx, message) + dag.BuildWarnings = append(dag.BuildWarnings, message) abortDef = spec.HandlerOn.Cancel }Not required for correctness but would help users migrate off
cancel.internal/integration/handler_on_test.go (2)
127-164: Add timeout protection to prevent hanging test.The concurrent abort test lacks timeout protection. If the DAG fails to complete, the test will hang indefinitely at line 157 (
<-done).Apply this diff to add timeout protection:
+ // Wait for the DAG to start running dag.AssertLatestStatus(t, core.Running) // Abort the DAG dagAgent.Abort() // Wait for completion - <-done + select { + case <-done: + // Success + case <-time.After(5 * time.Second): + t.Fatal("timeout waiting for DAG to complete after abort") + }Don't forget to add
"time"to the imports.
11-225: Consider table-driven test structure to reduce duplication.While the current subtest structure is clear, the coding guidelines recommend table-driven cases. A table-driven approach would reduce duplication in DAG YAML setup and status assertion patterns, making it easier to add new test cases.
Based on learnings, co-located Go tests should favour table-driven cases.
Example refactor:
func TestHandlerOn(t *testing.T) { t.Parallel() tests := []struct { name string dagYAML string runFunc func(*testing.T, *test.Agent) validateFunc func(*testing.T, *core.DAGRunStatus) }{ { name: "InitHandler_Success", dagYAML: ` handlerOn: init: command: "true" steps: - name: step1 command: "true" `, runFunc: func(t *testing.T, agent *test.Agent) { agent.RunSuccess(t) }, validateFunc: func(t *testing.T, status *core.DAGRunStatus) { require.Equal(t, core.Succeeded, status.Status) require.NotNil(t, status.OnInit) require.Equal(t, core.NodeSucceeded, status.OnInit.Status) }, }, // ... more test cases } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { th := test.Setup(t) dag := th.DAG(t, tt.dagYAML) agent := dag.Agent() tt.runFunc(t, agent) status := agent.Status(th.Context) tt.validateFunc(t, status) }) } }Note: The AbortHandler test would need special handling due to its concurrency pattern.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
internal/core/dag.go(2 hunks)internal/core/execution/runstatus.go(4 hunks)internal/core/spec/builder.go(2 hunks)internal/core/spec/definition.go(1 hunks)internal/integration/handler_on_test.go(1 hunks)internal/runtime/agent/agent.go(2 hunks)internal/runtime/runner.go(6 hunks)internal/runtime/transform/status.go(1 hunks)schemas/dag.schema.json(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*.go: Backend entrypoint incmd/orchestrates the scheduler and CLI; runtime, persistence, and service layers sit underinternal/*(for exampleinternal/runtime,internal/persistence)
Keep Go filesgofmt/goimportsclean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, andErr...names for package-level errors
Repository linting relies ongolangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers ininternal/common
Files:
internal/runtime/transform/status.gointernal/runtime/runner.gointernal/core/spec/definition.gointernal/core/execution/runstatus.gointernal/core/dag.gointernal/integration/handler_on_test.gointernal/runtime/agent/agent.gointernal/core/spec/builder.go
**/*_test.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*_test.go: Co-locate Go tests as*_test.go; favour table-driven cases and cover failure paths
Usestretchr/testify/requireand shared fixtures frominternal/testinstead of duplicating mocks
Files:
internal/integration/handler_on_test.go
🧠 Learnings (1)
📚 Learning: 2025-12-04T10:34:17.051Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.051Z
Learning: Applies to **/*_test.go : Co-locate Go tests as `*_test.go`; favour table-driven cases and cover failure paths
Applied to files:
internal/integration/handler_on_test.go
🧬 Code graph analysis (7)
internal/runtime/transform/status.go (2)
internal/core/execution/runstatus.go (1)
DAGRunStatus(35-57)internal/runtime/data.go (1)
NodeData(23-26)
internal/runtime/runner.go (7)
internal/core/step.go (1)
Step(13-77)internal/common/logger/tag/tag.go (3)
Step(25-27)Handler(332-334)Name(271-273)internal/core/dag.go (1)
HandlerOnInit(520-520)internal/common/logger/context.go (1)
Debug(35-37)internal/core/status.go (1)
NodeSkipped(58-58)internal/runtime/node.go (1)
Node(33-43)internal/runtime/data.go (2)
Data(17-20)NodeData(23-26)
internal/core/execution/runstatus.go (2)
internal/core/execution/node.go (2)
NewNodeOrNil(52-57)Node(9-24)internal/core/dag.go (1)
HandlerOn(476-482)
internal/core/dag.go (2)
api/v2/api.gen.go (1)
Step(781-844)internal/core/step.go (1)
Step(13-77)
internal/integration/handler_on_test.go (2)
internal/core/dag.go (1)
HandlerOn(476-482)internal/core/status.go (9)
Succeeded(11-11)NodeSucceeded(57-57)Aborted(10-10)NodeFailed(55-55)NodeNotStarted(53-53)NodeSkipped(58-58)NodeAborted(56-56)Running(8-8)Failed(9-9)
internal/runtime/agent/agent.go (4)
internal/runtime/transform/status.go (1)
WithOnInitNode(74-80)internal/core/dag.go (2)
HandlerOnInit(520-520)HandlerOn(476-482)api/v2/api.gen.go (1)
HandlerOn(447-459)api/v1/api.gen.go (1)
HandlerOn(358-370)
internal/core/spec/builder.go (2)
internal/core/dag.go (3)
HandlerOn(476-482)HandlerOnInit(520-520)HandlerOnCancel(523-523)api/v2/api.gen.go (1)
HandlerOn(447-459)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Test on ubuntu-latest
🔇 Additional comments (12)
internal/core/dag.go (1)
475-482: Init handler field and handler type constant are consistent with existing lifecycle hooks
HandlerOn.Initwithjson:"init,omitempty"plus theHandlerOnInitconstant mirrors the existing success/failure/cancel/exit pattern and lines up with the builder, runtime, and schema usage. No issues from a core-model perspective.Also applies to: 519-525
internal/core/spec/definition.go (1)
93-101: HandlerOn Init/Abort/Cancel definition cleanly matches intended YAML shapeThe added
InitandAbortfields plus the deprecation comment onCancelaccurately model the new lifecycle hooks and backward compatibility without impacting other spec fields.internal/runtime/transform/status.go (1)
73-80:WithOnInitNodecorrectly mirrors existing status optionsThe new option cleanly follows the existing pattern (OnExit/OnSuccess/OnFailure/OnCancel), safely handles nil, and ensures
OnInitinDAGRunStatusis populated from the runtime node when present.internal/core/execution/runstatus.go (1)
15-31: OnInit support in run status is wired consistentlyInitializing
OnInitinInitialStatus, adding theOnInitfield toDAGRunStatus, and extending bothErrors()andNodeByName()to include it keeps init handlers first‑class alongside exit/success/failure/cancel. The behavior is internally consistent and should integrate cleanly with the new status transformer option.Also applies to: 35-57, 65-88, 91-113
internal/runtime/agent/agent.go (1)
610-633: Agent wiring for Init handler into runner config and status looks correctPassing
a.dag.HandlerOn.Initintocfg.OnInitand addingtransform.WithOnInitNode(a.runner.HandlerNode(core.HandlerOnInit))alongside the existing handler options keeps Agent → Runner → Status behavior consistent with other lifecycle hooks. The ordering inStatus(nodes + log + init/exit/success/failure/cancel) is sensible.Also applies to: 744-780
internal/runtime/runner.go (1)
35-45: Init handler orchestration and handler precondition evaluation look correct; double‑check intended semanticsThe runtime changes hang together well:
Runner/Confignow carryonInit/OnInit, andsetup()registers anHandlerOnInitnode just like the other handlers.- In
Run, the init handler executes only after DAG-level preconditions pass and before any workflow nodes. IfrunEventHandlerreturns a non‑nil error (i.e., the init step’sExecutefails),setLastErrorandsetCanceledare called, so the event loop exits withcore.Abortedand no steps are run, matching the schema’s “if this fails, the DAG fails and no steps execute” intent.WithOnInitNode(r.HandlerNode(core.HandlerOnInit))ensures status objects carry the init handler node state, in line with OnExit/OnSuccess/OnFailure/OnCancel.Two subtle points you may want to confirm against your expectations/tests:
- In
runEventHandler, failures inPrepareor in evaluating handler preconditions now set the handler status (failed or skipped) but returnnil, so for the init handler only an actualExecuteerror will cancel the DAG. If you ever need preparatory/logging failures on init to abort the DAG too, you might want to propagate those as errors specifically forHandlerOnInit.setupEnvironEventHandlerseedsEnv[EnvKeyDAGRunStatus]fromr.Status(ctx, plan). ForonInitthis will typically beNotStartedat the time the handler runs, whereas for exit/success/failure/cancel it reflects the final DAG status. If you expect init scripts to branch on DAG status, it’s worth confirming that this asymmetry is acceptable.Overall, the init lifecycle hook is integrated consistently; just ensure these edge semantics line up with what you want long‑term (your new integration tests likely already cover most of this).
Also applies to: 66-76, 83-95, 128-143, 680-705, 724-743
internal/integration/handler_on_test.go (6)
1-9: LGTM: Clean test setup.Package naming and imports follow Go conventions and coding guidelines, properly using
stretchr/testify/requireas specified.
11-12: LGTM: Proper parallel test setup.Using
t.Parallel()for the integration test suite is appropriate and improves test execution time.
14-36: LGTM: Comprehensive happy path test.The test properly verifies both parsing (Init field and name) and runtime execution (OnInit status), with clear assertions.
38-67: LGTM: Thorough failure path test.Excellent coverage of the init failure scenario, verifying the abort status, exit handler execution, and that subsequent steps don't run. The failure path testing aligns with coding guidelines.
69-125: LGTM: Good edge case coverage for preconditions.Both tests properly handle precondition scenarios with appropriate assertions. The conditional checks (lines 114-116, 121-124) correctly handle timing-dependent behavior and are well-documented.
166-224: LGTM: Clear and consistent handler tests.The Failure, Success, and Exit handler tests follow a consistent pattern and properly verify each handler's execution and status.
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
internal/integration/handler_on_test.go(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*.go: Backend entrypoint incmd/orchestrates the scheduler and CLI; runtime, persistence, and service layers sit underinternal/*(for exampleinternal/runtime,internal/persistence)
Keep Go filesgofmt/goimportsclean; use tabs, PascalCase for exported symbols (SchedulerClient), lowerCamelCase for locals, andErr...names for package-level errors
Repository linting relies ongolangci-lint; prefer idiomatic Go patterns, minimal global state, and structured logging helpers ininternal/common
Files:
internal/integration/handler_on_test.go
**/*_test.go
📄 CodeRabbit inference engine (AGENTS.md)
**/*_test.go: Co-locate Go tests as*_test.go; favour table-driven cases and cover failure paths
Usestretchr/testify/requireand shared fixtures frominternal/testinstead of duplicating mocks
Files:
internal/integration/handler_on_test.go
🧠 Learnings (1)
📚 Learning: 2025-12-04T10:34:17.051Z
Learnt from: CR
Repo: dagu-org/dagu PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T10:34:17.051Z
Learning: Applies to **/*_test.go : Co-locate Go tests as `*_test.go`; favour table-driven cases and cover failure paths
Applied to files:
internal/integration/handler_on_test.go
🪛 GitHub Check: Go Linter
internal/integration/handler_on_test.go
[failure] 147-147:
unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
[failure] 115-115:
unused-parameter: parameter 't' seems to be unused, consider removing or renaming it as _ (revive)
[failure] 90-90:
unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
[failure] 60-60:
unused-parameter: parameter 't' seems to be unused, consider removing or renaming it as _ (revive)
[failure] 38-38:
unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Test on ubuntu-latest
🔇 Additional comments (5)
internal/integration/handler_on_test.go (5)
1-11: LGTM!The package declaration and imports are clean and follow Go best practices. The use of an external test package (
integration_test) is appropriate for integration tests.
13-196: Excellent test coverage for HandlerOn feature.The table-driven test structure is well-organized and covers the key scenarios: Init (success, failure, precondition skip, DAG precondition), Failure, Success, and Exit handlers. The use of optional setup/run/validate functions provides good flexibility.
48-77: Well-structured test for Init failure behavior.The test correctly verifies that Init handler failure causes DAG abortion, triggers the exit handler, and prevents step execution. The validation logic properly checks all expected states.
103-135: Good defensive handling of timing-dependent states.The test properly handles cases where OnInit may be nil or NotStarted when DAG preconditions fail, and accounts for timing-dependent node status (NotStarted or Aborted). This makes the test more robust.
216-257: Well-designed async abort test.The test properly handles the asynchronous nature of abort handling using a goroutine and synchronization channel. The pattern of waiting for the DAG to reach Running status before aborting ensures deterministic test behavior. The comment on line 231 helpfully clarifies the mapping between the YAML
abortfield and internalCancelhandler.
|
Can we create variables derived from parameters in the initHandler? Some example will be appreciable. |
|
@ghansham Yes, you can use |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1455 +/- ##
==========================================
- Coverage 59.86% 59.76% -0.10%
==========================================
Files 188 188
Lines 21167 21214 +47
==========================================
+ Hits 12671 12678 +7
- Misses 7180 7213 +33
- Partials 1316 1323 +7
... and 2 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
Summary by CodeRabbit
New Features
Documentation
Tests
✏️ Tip: You can customize this high-level summary in your review settings.