diff --git a/changelogs/fragments/81666-handlers-run_once.yml b/changelogs/fragments/81666-handlers-run_once.yml new file mode 100644 index 00000000000..e5cac9e2161 --- /dev/null +++ b/changelogs/fragments/81666-handlers-run_once.yml @@ -0,0 +1,2 @@ +bugfixes: + - Fix ``run_once`` being incorrectly interpreted on handlers (https://github.com/ansible/ansible/issues/81666) diff --git a/lib/ansible/playbook/handler.py b/lib/ansible/playbook/handler.py index 68970b4f1f0..2f2839813a7 100644 --- a/lib/ansible/playbook/handler.py +++ b/lib/ansible/playbook/handler.py @@ -53,6 +53,9 @@ class Handler(Task): def remove_host(self, host): self.notified_hosts = [h for h in self.notified_hosts if h != host] + def clear_hosts(self): + self.notified_hosts = [] + def is_host_notified(self, host): return host in self.notified_hosts diff --git a/lib/ansible/plugins/strategy/__init__.py b/lib/ansible/plugins/strategy/__init__.py index eda0e0af62e..d471effa2c3 100644 --- a/lib/ansible/plugins/strategy/__init__.py +++ b/lib/ansible/plugins/strategy/__init__.py @@ -800,10 +800,6 @@ class StrategyBase: ret_results.append(task_result) - if isinstance(original_task, Handler): - for handler in (h for b in iterator._play.handlers for h in b.block if h._uuid == original_task._uuid): - handler.remove_host(original_host) - if one_pass or max_passes is not None and (cur_pass + 1) >= max_passes: break @@ -1091,9 +1087,6 @@ class StrategyBase: header = skip_reason if skipped else msg display.vv(f"META: {header}") - if isinstance(task, Handler): - task.remove_host(target_host) - res = TaskResult(target_host, task, result) if skipped: self._tqm.send_callback('v2_runner_on_skipped', res) diff --git a/lib/ansible/plugins/strategy/free.py b/lib/ansible/plugins/strategy/free.py index fe6ad7df46f..82a21b1c3c1 100644 --- a/lib/ansible/plugins/strategy/free.py +++ b/lib/ansible/plugins/strategy/free.py @@ -146,6 +146,8 @@ class StrategyModule(StrategyBase): # advance the host, mark the host blocked, and queue it self._blocked_hosts[host_name] = True iterator.set_state_for_host(host.name, state) + if isinstance(task, Handler): + task.remove_host(host) try: action = action_loader.get(task.action, class_only=True, collection_list=task.collections) diff --git a/lib/ansible/plugins/strategy/linear.py b/lib/ansible/plugins/strategy/linear.py index 8ab3e3d5313..2fd4cbae3c2 100644 --- a/lib/ansible/plugins/strategy/linear.py +++ b/lib/ansible/plugins/strategy/linear.py @@ -242,6 +242,12 @@ class StrategyModule(StrategyBase): self._queue_task(host, task, task_vars, play_context) del task_vars + if isinstance(task, Handler): + if run_once: + task.clear_hosts() + else: + task.remove_host(host) + # if we're bypassing the host loop, break out now if run_once: break diff --git a/test/integration/targets/handlers/runme.sh b/test/integration/targets/handlers/runme.sh index 9b17dc7bbf3..757200de385 100755 --- a/test/integration/targets/handlers/runme.sh +++ b/test/integration/targets/handlers/runme.sh @@ -195,3 +195,6 @@ ansible localhost -m include_role -a "name=r1-dep_chain-vars" "$@" ansible-playbook test_include_tasks_in_include_role.yml "$@" 2>&1 | tee out.txt [ "$(grep out.txt -ce 'handler ran')" = "1" ] + +ansible-playbook test_run_once.yml -i inventory.handlers "$@" 2>&1 | tee out.txt +[ "$(grep out.txt -ce 'handler ran once')" = "1" ] diff --git a/test/integration/targets/handlers/test_run_once.yml b/test/integration/targets/handlers/test_run_once.yml new file mode 100644 index 00000000000..5418b46a6ff --- /dev/null +++ b/test/integration/targets/handlers/test_run_once.yml @@ -0,0 +1,10 @@ +- hosts: A,B,C + gather_facts: false + tasks: + - command: echo + notify: handler + handlers: + - name: handler + run_once: true + debug: + msg: handler ran once