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 namedRestart Checkoutand the task saidnotify: restart checkout.
New words, in plain English
- Handler - a task that runs only when notified, and only once per play, at the end.
notify- the directive that requests a handler by NAME.- Exact match - notify matches the handler's
namestring exactly - case and spacing included. - Flush - the point at which pending handlers actually run: after all tasks in the play.
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, pickansible-handler-notify, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.