Optimize playbook objects copying

ci_complete
This commit is contained in:
Martin Krizek 2026-05-26 13:58:02 +02:00
parent ba21909655
commit 5cde456833
No known key found for this signature in database
11 changed files with 73 additions and 148 deletions

View file

@ -202,8 +202,7 @@ class TaskContext(AmbientContextBase):
original_task = self.task
original_play_context = te._play_context
loop_item_task = self.task.copy(exclude_parent=True, exclude_tasks=True)
loop_item_task._parent = self.task._parent
loop_item_task = self.task.copy()
loop_item_play_context = te._play_context.copy()
self._task = loop_item_task

View file

@ -603,23 +603,36 @@ class PlayIterator:
if state.tasks_child_state:
state.tasks_child_state = self._insert_tasks_into_state(state.tasks_child_state, task_list)
else:
target_block = state._blocks[state.cur_block].copy(exclude_tasks=True)
target_block.block[state.cur_regular_task:state.cur_regular_task] = task_list
state._blocks[state.cur_block] = target_block
cur_block = state._blocks[state.cur_block]
if cur_block._copied:
cur_block.block[state.cur_regular_task:state.cur_regular_task] = task_list
else:
target_block = cur_block.copy()
target_block.block[state.cur_regular_task:state.cur_regular_task] = task_list
state._blocks[state.cur_block] = target_block
elif state.run_state == IteratingStates.RESCUE:
if state.rescue_child_state:
state.rescue_child_state = self._insert_tasks_into_state(state.rescue_child_state, task_list)
else:
target_block = state._blocks[state.cur_block].copy(exclude_tasks=True)
target_block.rescue[state.cur_rescue_task:state.cur_rescue_task] = task_list
state._blocks[state.cur_block] = target_block
cur_block = state._blocks[state.cur_block]
if cur_block._copied:
cur_block.rescue[state.cur_rescue_task:state.cur_rescue_task] = task_list
else:
target_block = cur_block.copy()
target_block.rescue[state.cur_rescue_task:state.cur_rescue_task] = task_list
state._blocks[state.cur_block] = target_block
elif state.run_state == IteratingStates.ALWAYS:
if state.always_child_state:
state.always_child_state = self._insert_tasks_into_state(state.always_child_state, task_list)
else:
target_block = state._blocks[state.cur_block].copy(exclude_tasks=True)
target_block.always[state.cur_always_task:state.cur_always_task] = task_list
state._blocks[state.cur_block] = target_block
cur_block = state._blocks[state.cur_block]
if cur_block._copied:
cur_block.rescue[state.cur_rescue_task:state.cur_rescue_task] = task_list
cur_block.always[state.cur_always_task:state.cur_always_task] = task_list
else:
target_block = cur_block.copy()
target_block.always[state.cur_always_task:state.cur_always_task] = task_list
state._blocks[state.cur_block] = target_block
elif state.run_state == IteratingStates.HANDLERS:
state.handlers[state.cur_handlers_task:state.cur_handlers_task] = [h for b in task_list for h in b.block]

View file

@ -9,9 +9,9 @@ import itertools
import operator
import os
import copy
import typing as t
from copy import copy as shallowcopy
from functools import cache
from ansible import constants as C
@ -423,29 +423,12 @@ class FieldAttributeBase:
self._squashed = True
def copy(self):
"""
Create a copy of this object and return it.
"""
try:
new_me = self.__class__()
except RecursionError as ex:
raise AnsibleError("Exceeded maximum object depth. This may have been caused by excessive role recursion.") from ex
new_me = copy.copy(self)
nd = new_me.__dict__
for name in self.fattributes:
setattr(new_me, name, shallowcopy(getattr(self, f'_{name}', Sentinel)))
new_me._loader = self._loader
new_me._variable_manager = self._variable_manager
new_me._origin = self._origin
new_me._validated = self._validated
new_me._finalized = self._finalized
new_me._uuid = self._uuid
# if the ds value was set on the object, copy it to the new copy too
if hasattr(self, '_ds'):
new_me._ds = self._ds
n = f'_{name}'
if (v := nd.get(n)) is not None and isinstance(v, (list, dict)):
nd[n] = v.copy()
return new_me
def get_validated_value(self, name, attribute, value, templar):

View file

@ -17,6 +17,8 @@
from __future__ import annotations
import itertools
from ansible.errors import AnsibleParserError
from ansible.module_utils.common.sentinel import Sentinel
from ansible.playbook.attribute import NonInheritableFieldAttribute
@ -40,13 +42,13 @@ class Block(Base, Conditional, CollectionSearch, Taggable, Notifiable, Delegatab
# similar to the 'else' clause for exceptions
# otherwise = FieldAttribute(isa='list')
def __init__(self, play=None, parent_block=None, role=None, task_include=None, use_handlers=False, implicit=False):
def __init__(self, play=None, parent_block=None, role=None, task_include=None, use_handlers=False):
self._play = play
self._role = role
self._parent = None
self._dep_chain = None
self._use_handlers = use_handlers
self._implicit = implicit
self._copied = False
if task_include:
self._parent = task_include
@ -83,8 +85,7 @@ class Block(Base, Conditional, CollectionSearch, Taggable, Notifiable, Delegatab
@staticmethod
def load(data, play=None, parent_block=None, role=None, task_include=None, use_handlers=False, variable_manager=None, loader=None):
implicit = not Block.is_block(data)
b = Block(play=play, parent_block=parent_block, role=role, task_include=task_include, use_handlers=use_handlers, implicit=implicit)
b = Block(play=play, parent_block=parent_block, role=role, task_include=task_include, use_handlers=use_handlers)
return b.load_data(data, variable_manager=variable_manager, loader=loader)
@staticmethod
@ -150,49 +151,32 @@ class Block(Base, Conditional, CollectionSearch, Taggable, Notifiable, Delegatab
else:
return self._dep_chain[:]
def copy(self, exclude_parent=False, exclude_tasks=False):
def _dupe_task_list(task_list, new_block):
def copy(self):
def _reparent_tasks(task_list, new_block):
new_task_list = []
for task in task_list:
new_task = task.copy(exclude_parent=True, exclude_tasks=exclude_tasks)
if task._parent:
new_task._parent = task._parent.copy(exclude_tasks=True)
if task._parent == new_block:
# If task._parent is the same as new_block, just replace it
new_task._parent = new_block
else:
# task may not be a direct child of new_block, search for the correct place to insert new_block
cur_obj = new_task._parent
while cur_obj._parent and cur_obj._parent != new_block:
cur_obj = cur_obj._parent
cur_obj._parent = new_block
else:
new_task = task.copy()
if task._parent == new_block:
# If task._parent is the same as new_block, just replace it
new_task._parent = new_block
else:
# parent is include/import, skip one level
new_task._parent._parent = new_block
new_task_list.append(new_task)
return new_task_list
new_me = super(Block, self).copy()
new_me._play = self._play
new_me._use_handlers = self._use_handlers
new_me = super().copy()
if self._dep_chain is not None:
new_me._dep_chain = self._dep_chain[:]
new_me._parent = None
if self._parent and not exclude_parent:
new_me._parent = self._parent.copy(exclude_tasks=True)
# re-parent tasks within the block to point at the new one via _parent
new_me.block = _reparent_tasks(self.block, new_me)
new_me.rescue = _reparent_tasks(self.rescue, new_me)
new_me.always = _reparent_tasks(self.always, new_me)
if not exclude_tasks:
new_me.block = _dupe_task_list(self.block or [], new_me)
new_me.rescue = _dupe_task_list(self.rescue or [], new_me)
new_me.always = _dupe_task_list(self.always or [], new_me)
new_me._copied = True
new_me._role = None
if self._role:
new_me._role = self._role
new_me.validate()
return new_me
def set_loader(self, loader):
@ -289,40 +273,26 @@ class Block(Base, Conditional, CollectionSearch, Taggable, Notifiable, Delegatab
tmp_list = []
for task in target:
if isinstance(task, Block):
filtered_block = evaluate_block(task)
filtered_block = task.filter_tagged_tasks(all_vars)
if filtered_block.has_tasks():
tmp_list.append(filtered_block)
elif task.evaluate_tags(self._play.only_tags, self._play.skip_tags, all_vars=all_vars):
tmp_list.append(task)
return tmp_list
def evaluate_block(block):
new_block = block.copy(exclude_parent=True, exclude_tasks=True)
new_block._parent = block._parent
new_block.block = evaluate_and_append_task(block.block)
new_block.rescue = evaluate_and_append_task(block.rescue)
new_block.always = evaluate_and_append_task(block.always)
return new_block
return evaluate_block(self)
self.block = evaluate_and_append_task(self.block)
self.rescue = evaluate_and_append_task(self.rescue)
self.always = evaluate_and_append_task(self.always)
return self # FIXME
def get_tasks(self):
def evaluate_and_append_task(target):
tmp_list = []
for task in target:
if isinstance(task, Block):
tmp_list.extend(evaluate_block(task))
else:
tmp_list.append(task)
return tmp_list
def evaluate_block(block):
rv = evaluate_and_append_task(block.block)
rv.extend(evaluate_and_append_task(block.rescue))
rv.extend(evaluate_and_append_task(block.always))
return rv
return evaluate_block(self)
task_list = []
for task in itertools.chain(self.block, self.rescue, self.always):
if isinstance(task, Block):
task_list.extend(task.get_tasks())
else:
task_list.append(task)
return task_list
def has_tasks(self):
return len(self.block) > 0 or len(self.rescue) > 0 or len(self.always) > 0

View file

@ -219,13 +219,11 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
# nested includes, and we want the include order printed correctly
display.vv("statically imported: %s" % include_file)
ti_copy = task.copy(exclude_parent=True)
ti_copy._parent = block
included_blocks = load_list_of_blocks(
data,
play=play,
parent_block=None,
task_include=ti_copy,
task_include=task,
role=role,
use_handlers=use_handlers,
loader=loader,

View file

@ -320,7 +320,7 @@ class Play(Base, Taggable, CollectionSearch):
if self.pre_tasks:
b.block = self.pre_tasks
else:
nt = noop_task.copy(exclude_parent=True)
nt = noop_task.copy()
nt._parent = b
b.block = [nt]
b.always = [flush_block]
@ -331,7 +331,7 @@ class Play(Base, Taggable, CollectionSearch):
if tasks:
b.block = tasks
else:
nt = noop_task.copy(exclude_parent=True)
nt = noop_task.copy()
nt._parent = b
b.block = [nt]
b.always = [flush_block]
@ -341,7 +341,7 @@ class Play(Base, Taggable, CollectionSearch):
if self.post_tasks:
b.block = self.post_tasks
else:
nt = noop_task.copy(exclude_parent=True)
nt = noop_task.copy()
nt._parent = b
b.block = [nt]
b.always = [flush_block]
@ -385,12 +385,8 @@ class Play(Base, Taggable, CollectionSearch):
return tasklist
def copy(self):
new_me = super(Play, self).copy()
new_me = super().copy()
new_me.role_cache = self.role_cache.copy()
new_me._included_conditional = self._included_conditional
new_me._included_path = self._included_path
new_me._action_groups = self._action_groups
new_me._group_actions = self._group_actions
return new_me
def _post_validate_validate_argspec(self, attr: NonInheritableFieldAttribute, value: object, templar: _TE) -> str | None:

View file

@ -601,10 +601,8 @@ class Role(Base, Conditional, Taggable, CollectionSearch, Delegatable):
block_list.extend(dep_blocks)
for task_block in self._handler_blocks:
new_task_block = task_block.copy()
new_task_block._dep_chain = new_dep_chain
new_task_block._play = play
block_list.append(new_task_block)
task_block._dep_chain = new_dep_chain
block_list.append(task_block)
return block_list
@ -642,10 +640,8 @@ class Role(Base, Conditional, Taggable, CollectionSearch, Delegatable):
block_list.extend(dep_blocks)
for task_block in self._task_blocks:
new_task_block = task_block.copy()
new_task_block._dep_chain = new_dep_chain
new_task_block._play = play
block_list.append(new_task_block)
task_block._dep_chain = new_dep_chain
block_list.append(task_block)
eor_block = Block(play=play)
eor_block._loader = self._loader

View file

@ -164,15 +164,9 @@ class IncludeRole(TaskInclude):
return ir
def copy(self, exclude_parent=False, exclude_tasks=False):
new_me = super(IncludeRole, self).copy(exclude_parent=exclude_parent, exclude_tasks=exclude_tasks)
new_me.statically_loaded = self.statically_loaded
def copy(self):
new_me = super().copy()
new_me._from_files = self._from_files.copy()
new_me._parent_role = self._parent_role
new_me._role_name = self._role_name
new_me._role_path = self._role_path
return new_me
def get_include_params(self):

View file

@ -485,23 +485,6 @@ class Task(Base, Conditional, Taggable, CollectionSearch, Notifiable, Delegatabl
all_vars |= self.vars
return all_vars
def copy(self, exclude_parent: bool = False, exclude_tasks: bool = False) -> Task:
new_me = super(Task, self).copy()
new_me._parent = None
if self._parent and not exclude_parent:
new_me._parent = self._parent.copy(exclude_tasks=exclude_tasks)
new_me._role = None
if self._role:
new_me._role = self._role
new_me.implicit = self.implicit
new_me._resolved_action = self._resolved_action
new_me._uuid = self._uuid
return new_me
def set_loader(self, loader):
"""
Sets the loader on this object and recursively on parent, child objects.

View file

@ -98,11 +98,6 @@ class TaskInclude(Task):
return ds
def copy(self, exclude_parent=False, exclude_tasks=False):
new_me = super(TaskInclude, self).copy(exclude_parent=exclude_parent, exclude_tasks=exclude_tasks)
new_me.statically_loaded = self.statically_loaded
return new_me
def build_parent_block(self):
"""
This method is used to create the parent block for the included tasks

View file

@ -451,8 +451,7 @@ class StrategyBase:
task = Task()
else:
task = found_task.copy(exclude_parent=True, exclude_tasks=True)
task._parent = found_task._parent
task = found_task.copy()
task.from_attrs(wire_task_result.task_fields)
@ -773,8 +772,7 @@ class StrategyBase:
"""
A proven safe and performant way to create a copy of an included file
"""
ti_copy = included_file._task.copy(exclude_parent=True)
ti_copy._parent = included_file._task._parent
ti_copy = included_file._task.copy()
temp_vars = ti_copy.vars | included_file._vars