Open-Source Wikis

/

Ansible

/

Systems

/

Executor

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) WorkerProcess children, each connected by an input queue and a shared multiprocessing.Queue for results.
  • Loads the strategy plugin named in the play (default linear) via strategy_loader.
  • Loads the configured callback plugins via callback_loader.
  • Sets up an AggregateStats to 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 to rescue:; always: runs unconditionally at the end.
  • Rescue clears the failure for the host (it goes back to ok status with rescued+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:

  1. Pull a (host, task, play_context, task_vars) tuple from its input queue.
  2. Construct a TaskExecutor for it.
  3. Call TaskExecutor.run() and capture the result.
  4. 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 via Conditional and 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| TQM

The 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:StrategyBase first.
  • 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.py for the wire format and task_queue_manager.py for the dispatch.
  • Debugging task scheduling → set strategy: debug in the play and use the interactive prompt; or wire up display.vvv calls in play_iterator.py.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Executor – Ansible wiki | Factory