Repository: ansible/ansible

Issue:
handlers executed repeatedly
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
handlers

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.3.0 (devel 5f0815d7f4) last updated 2016/12/22 19:01:55 (GMT +000)
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides

this is the latest devel branch from "git pull"
```

##### CONFIGURATION
<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->

##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
-->
Ubuntu 14.04.1 , Centos 7.1

##### SUMMARY
<!--- Explain the problem briefly -->
Handlers should only be triggered once, per the documentation, but they are triggered more than once.

"These ‘notify’ actions are triggered at the end of each block of tasks in a play, and will only be triggered once even if notified by multiple different tasks.  For instance, multiple resources may indicate that apache needs to be restarted because they have changed a config file, but apache will only be bounced once to avoid unnecessary restarts."
##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->
create a role, as follows:
<!--- Paste example playbooks or commands between quotes below -->
```yaml
test/tasks/main.yml:
---
- shell: echo testing
  notify: handler1

- shell: echo testing
  notify: handler1

- shell: echo testing
  notify: handler1
  
test/handlers/main.yml
---
- name: handler1
  shell: echo "1" >> /tmp/test.txt
  
test/test.yml:
---
    - hosts: all
      remote_user: ansible
      become: true
      roles:
        - { role: 'test' }
```

run the playbook:
cd test 
ansible-playbook test.yml

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
/tmp/test.txt would contain a single "1"
##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
/tmp/test.txt has three "1" values in it, corresponding to the handler running three times.

<!--- Paste verbatim command output between quotes below -->
```
ansible-playbook test.yml

PLAY [all] *********************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************
ok: [localhost]

TASK [test : command] **********************************************************************************************************
GOT A NOTIFY
TRYING TO SEND NOTIFICATION TO HANDLER: handler1
changed: [localhost]

TASK [test : command] **********************************************************************************************************
GOT A NOTIFY
TRYING TO SEND NOTIFICATION TO HANDLER: handler1
changed: [localhost]

TASK [test : command] **********************************************************************************************************
GOT A NOTIFY
TRYING TO SEND NOTIFICATION TO HANDLER: handler1
changed: [localhost]

RUNNING HANDLER [test : handler1] **********************************************************************************************
changed: [localhost]
changed: [localhost]
changed: [localhost]

PLAY RECAP *********************************************************************************************************************
localhost                  : ok=7    changed=6    unreachable=0    failed=0

```



Discussion:
Verified broken in devel and on 2.2.1- should be addressed before next RC (including an integration test). 

Resolving patch:
diff --git a/lib/ansible/plugins/strategy/__init__.py b/lib/ansible/plugins/strategy/__init__.py
--- a/lib/ansible/plugins/strategy/__init__.py
+++ b/lib/ansible/plugins/strategy/__init__.py
@@ -437,7 +437,7 @@ def parent_handler_match(target_handler, handler_name):
                                 target_handler = search_handler_blocks_by_name(handler_name, iterator._play.handlers)
                                 if target_handler is not None:
                                     found = True
-                                    if original_host._uuid not in self._notified_handlers[target_handler._uuid]:
+                                    if original_host not in self._notified_handlers[target_handler._uuid]:
                                         self._notified_handlers[target_handler._uuid].append(original_host)
                                         # FIXME: should this be a callback?
                                         display.vv("NOTIFIED HANDLER %s" % (handler_name,))


Test patch:
diff --git a/test/integration/targets/handlers/roles/test_handlers_meta/handlers/main.yml b/test/integration/targets/handlers/roles/test_handlers_meta/handlers/main.yml
--- a/test/integration/targets/handlers/roles/test_handlers_meta/handlers/main.yml
+++ b/test/integration/targets/handlers/roles/test_handlers_meta/handlers/main.yml
@@ -5,3 +5,6 @@
 - name: set_handler_fact_2
   set_fact:
     handler2_called: True
+
+- name: count_handler
+  shell: echo . >> {{ handler_countpath }}
diff --git a/test/integration/targets/handlers/roles/test_handlers_meta/tasks/main.yml b/test/integration/targets/handlers/roles/test_handlers_meta/tasks/main.yml
--- a/test/integration/targets/handlers/roles/test_handlers_meta/tasks/main.yml
+++ b/test/integration/targets/handlers/roles/test_handlers_meta/tasks/main.yml
@@ -30,6 +30,41 @@
         - "handler1_called is defined"
         - "handler2_called is not defined"
 
+- name: make a tempfile for counting
+  shell: mktemp
+  register: mktemp_out
+
+- name: register tempfile path
+  set_fact:
+    handler_countpath: "{{ mktemp_out.stdout }}"
+
+- name: notify the counting handler
+  shell: echo
+  notify:
+    - count_handler
+
+- name: notify the counting handler again
+  shell: echo
+  notify:
+    - count_handler
+
+- name: force handler execution now
+  meta: flush_handlers
+
+- name: get handler execution count
+  shell: cat {{ handler_countpath }} | grep -o . | wc -l
+  register: exec_count_out
+
+- debug: var=exec_count_out.stdout
+
+- name: ensure single execution
+  assert:
+    that:
+    - exec_count_out.stdout | int == 1
+
+- name: cleanup tempfile
+  file: path={{ handler_countpath }} state=absent
+
 - name: reset handler1_called
   set_fact:
     handler1_called: False