ansible/ansible
Executor
The execution engine. Turns a parsed playbook into work for a pool of forked worker processes, applies a scheduling strategy, and routes results back through callback plugins. Lives in lib/ansible/executor/ with the strategy plugins in lib/ansible/plugins/strategy/ and the underlying task representation in lib/ansible/_internal/_task.py.
Component map
| Component | File | Lines | Role |
|---|---|---|---|
PlaybookExecutor |
lib/ansible/executor/playbook_executor.py |
322 | Per-playbook driver; iterates plays, applies serial/percentage batching, owns one TQM |
TaskQueueManager |
lib/ansible/executor/task_queue_manager.py |
525 | Per-run scheduler; forks workers, runs strategy, drains result queue |
PlayIterator |
lib/ansible/executor/play_iterator.py |
1029 | Per-host state machine over tasks/blocks/handlers/rescue/always |
WorkerProcess |
lib/ansible/executor/process/worker.py |
~500 | Forked child running TaskExecutor |
TaskExecutor |
lib/ansible/executor/task_executor.py |
1075 | Runs one (host, task) pair end-to-end |
module_common |
lib/ansible/executor/module_common.py |
1694 | AnsiBallZ wrapper builder (see its own page) |
AggregateStats |
lib/ansible/executor/stats.py |
100 | Per-host counters of ok/changed/unreachable/failed/skipped/rescued/ignored |
CallbackTaskResult / WireTaskResult |
lib/ansible/executor/task_result.py + lib/ansible/_internal/_task.py |
1444 (combined) | Result objects passed across the worker boundary |
| Strategy plugins | lib/ansible/plugins/strategy/{linear,free,host_pinned,debug}.py |
395+ | Scheduling policy |
How it works
graph TD
PE[PlaybookExecutor] -->|for each play| BATCH[Batch hosts: serial/percentage]
BATCH --> TQM[TaskQueueManager]
TQM -->|fork| W1[WorkerProcess 1]
TQM -->|fork| W2[WorkerProcess 2]
TQM -->|fork| WN[WorkerProcess N]
TQM --> STRAT[Strategy plugin: linear/free/host_pinned/debug]
STRAT --> ITER[PlayIterator: per-host state]
ITER -->|next host, task| QUEUE[Worker input queue]
QUEUE --> W1
QUEUE --> W2
QUEUE --> WN
W1 -->|TaskExecutor.run| TE1[TaskExecutor]
W2 -->|TaskExecutor.run| TE2[TaskExecutor]
WN -->|TaskExecutor.run| TEN[TaskExecutor]
TE1 --> ACTION1[Action plugin]
TE2 --> ACTION2[Action plugin]
TEN --> ACTIONN[Action plugin]
ACTION1 -->|JSON result| RQ[Result queue]
ACTION2 -->|JSON result| RQ
ACTIONN -->|JSON result| RQ
RQ --> TQM
TQM -->|callback events| CB[Callback plugins]1. PlaybookExecutor opens the run
PlaybookExecutor.__init__ (lib/ansible/executor/playbook_executor.py) takes the list of parsed playbooks, the inventory, the variable manager, and the password set. Unless the user passed a list-only flag (--list-hosts/--list-tasks/--list-tags/--syntax-check), it constructs one TaskQueueManager. The executor's .run() walks each play, applies batching (the serial: keyword), and calls tqm.run(play) per batch.
2. TaskQueueManager constructs the worker pool
The TQM (task_queue_manager.py) is the heart of execution:
- Forks
--forks(default 5)WorkerProcesschildren, each connected by an input queue and a sharedmultiprocessing.Queuefor results. - Loads the strategy plugin named in the play (default
linear) viastrategy_loader. - Loads the configured callback plugins via
callback_loader. - Sets up an
AggregateStatsto track per-host counters.
It exposes a FinalQueue (used by workers to deliver results) and a per-host PlayContext factory.
3. PlayIterator walks the task tree
PlayIterator (play_iterator.py) is the hardest-working piece. For each host, it tracks an IteratingStates enum that says where in the play this host currently is: ITERATING_SETUP, ITERATING_TASKS, ITERATING_RESCUE, ITERATING_ALWAYS, or ITERATING_HANDLERS. The strategy calls iterator.get_next_task_for_host(host) to advance state and pull the next task.
Block semantics (lib/ansible/playbook/block.py):
- A block runs its
block:tasks; if any fails, it switches torescue:;always:runs unconditionally at the end. - Rescue clears the failure for the host (it goes back to
okstatus withrescued+1). - Includes (
include_tasks,include_role) are dynamic — the iterator pauses, resolves the include, and splices the new tasks in.
Handlers run at the end of the play (or on meta: flush_handlers) over hosts that registered notifications. The iterator switches to ITERATING_HANDLERS and drains the handler list.
4. The strategy plugin schedules dispatch
Strategies (lib/ansible/plugins/strategy/) decide which (host, task) pair to dispatch next:
| Strategy | Behavior |
|---|---|
linear |
All hosts run the same task in lockstep. The slowest host blocks the next task. The default. |
free |
Each host runs as fast as it can. Tasks proceed independently per host. |
host_pinned |
Like linear, but each worker process is dedicated to one host for the duration of the play. |
debug |
Like linear, but drops into an interactive prompt on failure. See Debugging. |
All strategies inherit from StrategyBase in lib/ansible/plugins/strategy/__init__.py. They consume the iterator, push tasks into worker queues, and drain results into the TQM's _results queue. A strategy plugin owns the main "next task → dispatch → wait → process result" loop.
5. WorkerProcess + TaskExecutor
Each WorkerProcess (a multiprocessing.Process subclass at lib/ansible/executor/process/worker.py) sits in a loop:
- Pull a
(host, task, play_context, task_vars)tuple from its input queue. - Construct a
TaskExecutorfor it. - Call
TaskExecutor.run()and capture the result. - Wrap the result in a
WireTaskResult(lib/ansible/_internal/_task.py) and push it to the shared result queue.
TaskExecutor (task_executor.py, 1075 lines) is the heaviest single piece of the controller. It:
- Evaluates the task's
when:conditional viaConditionaland the templating engine. - Expands the task's
loop:into per-iteration sub-tasks. - Resolves the action plugin name via the action loader (which honors FQCNs and the redirect map).
- Loads the connection plugin (with become wrapper if requested).
- Calls the action plugin's
run(). - Handles
register:,until:/retries:,failed_when:,changed_when:. - Returns a result dict.
For module-shipping actions (the common case), the action plugin's run() calls self._execute_module(), which invokes module_common.modify_module() to build an AnsiBallZ archive and pushes it through the connection plugin. See Module execution and AnsiBallZ.
6. Callbacks
Results return to the TQM via the _results queue. The TQM's main loop pulls results, updates AggregateStats, and emits events to every loaded callback plugin: v2_runner_on_ok, v2_runner_on_failed, v2_playbook_on_task_start, etc. Callback plugins implement only the events they care about. The default human-readable callback prints the colored "task: TASK [name] *** ok: [hostname]" output that ansible-playbook is known for.
See Plugins → Callback for the callback plugin spec.
Process model and queues
graph LR
subgraph Controller process
TQM[TQM main loop]
STRAT[Strategy]
end
subgraph Worker process 1
TE1[TaskExecutor]
end
subgraph Worker process N
TEN[TaskExecutor]
end
TQM -.->|input queue 1| TE1
TQM -.->|input queue N| TEN
TE1 -.->|FinalQueue| TQM
TEN -.->|FinalQueue| TQMThe shared FinalQueue is a multiprocessing.Queue used for everything coming back from workers: task results, display messages (DisplaySend), prompt requests (PromptSend), and callback dispatches (CallbackSend). The TQM's main loop calls final_q.get() repeatedly and routes each message type to the appropriate handler.
The forked workers inherit the controller's loaded plugins (Python's fork() copy-on-write), so they don't pay startup cost per task. They serialize results back through the queue rather than sharing memory.
Variables across the boundary
A subtle point: tasks shipped to workers carry their already-templated variables. The worker doesn't refetch host vars from VariableManager; it gets a snapshot built by the strategy. This is necessary because workers need stable, deterministic data — but it also means set_fact results have to flow back to the TQM via the result queue and be merged into the VariableManager before the next task's vars are computed.
HostTaskResult and UnifiedTaskResult (lib/ansible/_internal/_task.py) represent the cross-boundary view; the TQM materializes a CallbackTaskResult on the controller side for callback consumption.
Integration points
- Imports from:
lib/ansible/playbook/(Task, Block, Play),lib/ansible/plugins/loader.py,lib/ansible/plugins/strategy/,lib/ansible/inventory/manager.py,lib/ansible/vars/manager.py,lib/ansible/_internal/_task.py,lib/ansible/_internal/_templating/_engine.py. - Imported by: every workload CLI (
ansible-playbook,ansible,ansible-pull,ansible-console). - Plugins it loads:
strategy_loader,callback_loader,connection_loader(preloaded for caching),become_loader,shell_loader,module_loader,action_loader.
Entry points for modification
- Adding scheduling behavior → write a new strategy plugin in a collection. Read
lib/ansible/plugins/strategy/__init__.py:StrategyBasefirst. - Inserting cross-cutting logic at every task → an action plugin or a callback plugin is the right answer, not a TQM modification.
- Changing how results flow → look at
lib/ansible/_internal/_task.pyfor the wire format andtask_queue_manager.pyfor the dispatch. - Debugging task scheduling → set
strategy: debugin the play and use the interactive prompt; or wire updisplay.vvvcalls inplay_iterator.py.
Cross-links
- Module execution and AnsiBallZ — what
TaskExecutorships to remote hosts. - Plugin loader — how the executor finds strategy/callback/action plugins.
- Plugins → Strategy — the user-facing scheduling extension point.
- Plugins → Callback — the event consumer side.
- Primitives → Playbook and play — what feeds in.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.