notify and the Silent No-Op

Scenario: Operators changed a listen port. The playbook ran green. The service kept serving the old port for three hours, because the handler that should have restarted it was named Restart Checkout and the task said notify: restart checkout.

New words, in plain English

A handler is a task that runs only if something asked for it, and only once no matter how many tasks asked. That is exactly right for service restarts: three config files may change, but the service should bounce once.

Two rules govern the mechanism, and both cause outages when broken.

A handler only runs if the notifying task reported changed. A task that reports ok notifies nothing. This is the desirable behaviour - no change, no restart - and it is also why a non-idempotent task that always reports changed will bounce your service on every single run.

notify matches the handler name exactly. restart checkout does not match a handler named Restart Checkout. There is no warning and no error; the notification simply finds nothing. In older Ansible this was silent, and even where a warning exists it is easy to miss in a long run. The result is the scenario above: a green playbook, a changed config file, and a process still running the old one.

The defence is listen. A handler can declare listen: restart web stack, and any number of tasks can notify that topic. It decouples the notification from the handler's display name and makes several handlers respond to one event.

Analogy: notify is a fire alarm wired to a specific room number. Change the room number on the panel but not on the alarm and nothing happens when the alarm sounds - no error, no siren, just a fire.

A worked example

- name: Render the checkout configuration
  ansible.builtin.template:
    src: checkout.conf.j2
    dest: /etc/checkout/checkout.conf
  notify: restart checkout          # must match EXACTLY

- name: Render the TLS configuration
  ansible.builtin.template:
    src: tls.conf.j2
    dest: /etc/checkout/tls.conf
  notify: restart checkout          # same handler - still ONE restart

handlers:
  - name: restart checkout
    ansible.builtin.service:
      name: checkout
      state: restarted

  # More robust: a topic several tasks and handlers can share
  - name: restart checkout
    listen: checkout changed
    ansible.builtin.service:
      name: checkout
      state: restarted

Handlers run at the end of the play, after every task, in the order the handlers are defined - not the order they were notified. If you need a restart to happen before a later task, meta: flush_handlers forces the pending handlers to run at that exact point.

And the rule from Phase 2's failure section applies here with real consequences: if the play fails before the flush, notified handlers do not run at all. Configuration on disk changed, the process never picked it up, and the two disagree. force_handlers: true is the override when the restart is the safer outcome.

Warning: A mistyped notify produces a fully green run in which the restart never happened. Prove the handler actually fires - do not assume it from a green recap.
Goal: Put this to work in the ansible-handler-notify lab. Open /labs/ansible, pick ansible-handler-notify, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.