Add automatic workflow node retries via a max_retries node setting#564
Conversation
There was a problem hiding this comment.
Pull request overview
Adds automatic per-node workflow retries controlled by a reserved variable (ascender_max_retries), integrating the retry semantics into the DAG scheduler and ensuring retried attempts remain linked/protected for traceability and deletion safety.
Changes:
- Introduces
WorkflowJobNoderetry state (retry_attempts,retried_jobs) and scheduler logic to respawn failed/error nodes while budget remains. - Strips
ascender_max_retriesfrom spawned jobextra_varsand relaxes node serializer prompt-validation to allow the reserved var inextra_data. - Adds unit/functional coverage and documents the new “Automatic Node Retries” behavior.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| docs/workflow.md | Documents reserved-var configuration, semantics, and exclusions for automatic node retries. |
| awx/main/tests/unit/scheduler/test_dag_workflow.py | Unit tests for DAG behavior when retries are pending/exhausted (including convergence blocking). |
| awx/main/tests/unit/models/test_workflow_unit.py | Unit tests for retry budget resolution/precedence and helper methods (retry_pending, etc.). |
| awx/main/tests/functional/task_management/test_scheduler.py | End-to-end TaskManager/WorkflowManager lifecycle tests for respawn and exhaustion. |
| awx/main/tests/functional/models/test_workflow.py | Functional DAG tests validating respawn behavior, template deletion, and per-node overrides. |
| awx/main/tests/functional/api/test_workflow_node.py | Functional API tests for reserved-var acceptance and deletion protection of superseded attempts. |
| awx/main/scheduler/task_manager.py | Implements retry respawn mechanics, increments counters, and tracks superseded jobs. |
| awx/main/scheduler/dag_workflow.py | Centralizes “node finished/final failure” logic via WorkflowJobNode helper methods. |
| awx/main/models/workflow.py | Adds retry fields/constants and implements max-retry resolution + helper predicates + var stripping. |
| awx/main/models/unified_jobs.py | Preserves workflow linkage for superseded attempts via retried_workflow_nodes. |
| awx/main/migrations/0201_workflowjobnode_retry_attempts.py | Migration adding retry_attempts and retried_jobs to WorkflowJobNode. |
| awx/api/views/mixin.py | Prevents deletion of superseded attempts while their workflow job is still active. |
| awx/api/serializers.py | Allows reserved retry var through node prompt validation; exposes retry_attempts/retried_jobs. |
|
Needs rebasing and fixing the migration name/dependency, then I will review. But also a thought, why use an extra var instead of creating an actual workflow setting? I really dislike relying on user supplied ansible variables for things like this. |
Workflow nodes get a max_retries setting (0-100, default 0). While a failed or errored node has retries left the scheduler treats it as still running instead of following its failure paths, and respawns its job on the next cycle with the same launch configuration. The node tracks the attempt count in retry_attempts and keeps superseded attempts in retried_jobs, which stay protected from deletion while the workflow runs. Canceled jobs, approval nodes, deleted templates and jobs that never started are not retried. The field lives on both the template node and the job node and is copied at launch, editable through the API, awxkit and the node form in the workflow visualizer.
952eb0a to
76fee5a
Compare
|
Rebased onto main and renumbered the migration to 0203 with its dependency on 0202_project_webhooks. On the extra var concern: you were right, and it turned out to be less work than expected because the variable plumbing was most of the complexity. What it adds instead: the field in both node serializers, awxkit passthrough, and a Max Retries number input in the visualizer node form (hidden for approval nodes since those never retry) plus the value in the node details view. I squashed everything into one commit since the old ascender_max_retries history no longer made sense, and updated the PR description. Backend suites (workflow unit, functional models/api/scheduler), jest on the touched screens, flake8/black and makemigrations --check are all green. Ready for review. |
| timeoutMinutes: 0, | ||
| timeoutSeconds: 0, | ||
| convergence: 'any', | ||
| maxRetries: nodeToEdit?.max_retries || 0, |
Summary
Workflow nodes get a
max_retriessetting (0 to 100, default 0): when the node's job fails, the scheduler respawns it automatically up to that many times before following the node's failure paths.Each node keeps its own independent budget, so one flaky node does not eat the retries of its siblings. This covers the classic "the playbook failed because the network hiccuped, run it again" case that today means either failing the whole branch or relaunching by hand. It complements the relaunch-from-failed feature: retries are automatic and happen inside the run, relaunch is manual and happens after it. Relaunched workflows start their nodes with fresh counters.
Earlier revisions of this PR drove this through a reserved
ascender_max_retriesextra var. Following review feedback it is now a proper field on the workflow node, which killed all the variable plumbing: no value parsing or clamping, no stripping the var before it reaches playbooks and nested or sliced workflows, no serializer exemption for unprompted vars. The field is validated at the API boundary (0 to 100, capped because there is no delay between attempts) and is editable from the node form in the visualizer.How it behaves
max_retrieslives on the workflow job template node and is copied to the workflow job node at launch, likeall_parents_must_converge, so editing the template does not affect running workflows. Copying a workflow job template preserves itretry_attemptscounter goes upjobfield always points at the newest attempt. Superseded attempts are kept in a newretried_jobsrelation on the node, so they stay linked to their workflow (source_workflow_jobstill resolves) and cannot be deleted while the workflow is runningDesign notes
WorkflowJobNode(retry_pending(),job_finished(),finally_failed()) and the DAG predicates indag_workflow.pyconsume those instead of each keeping its own status list, so the invariant has one homespawn_workflow_graph_jobs: a node coming back frombfs_nodes_to_run()with a job attached is a retry, so the old job goes intoretried_jobs, the counter increments and a new unified job is created exactly like the first attempt (fresh ancestor artifacts included)select_related('job')to the scheduler queryset (that returns baseUnifiedJobinstances, there is already a warning about it in the file)max_retriesto both node tables plusretry_attemptsand theretried_jobstable on the job nodedocs/workflow.mdTests