Ordering, Flushing and Multiple Handlers

Scenario: A certificate is renewed and the web server must reload before a later task runs a TLS smoke test. The test runs first, against the old certificate, and fails.

New words, in plain English

Handler ordering trips people up because it is not the order you would guess. Handlers run in the order they are defined in the handlers section, regardless of the order in which they were notified. If a reload must precede a restart, define the reload first - notifying it first changes nothing.

When work later in the same play depends on the handler having already run, meta: flush_handlers is the tool. It is a task like any other, placed exactly where the flush is needed, and it runs every pending handler at that point.

listen inverts the relationship usefully. Instead of tasks naming handlers, handlers subscribe to a topic. One notify: tls material changed can trigger a certificate reload, a proxy reload and a cache flush, each defined independently. Adding a fourth subscriber later requires no change to the notifying task at all - which matters a great deal in roles other teams consume.

Handlers can also notify other handlers, giving you an ordered chain: reload config notifies verify service, for example. Keep chains short; they are easy to write and hard to read.

Analogy: Handlers are the end-of-meeting actions list. They are worked through in the order written on the agenda, not the order people raised them. flush_handlers is calling a recess to deal with one item now, because the rest of the meeting depends on it.

A worked example

- name: Deploy the certificate
  ansible.builtin.copy:
    src: app.pem
    dest: /etc/ssl/app.pem
    mode: "0600"
  notify: tls material changed        # a TOPIC, not a handler name

- name: Make the reload happen NOW, not at the end of the play
  ansible.builtin.meta: flush_handlers

- name: Smoke-test TLS with the new certificate
  ansible.builtin.command: /opt/bin/tls-check.sh
  changed_when: false

handlers:
  - name: reload nginx               # defined FIRST, so runs FIRST
    listen: tls material changed
    ansible.builtin.service: { name: nginx, state: reloaded }

  - name: flush the edge cache
    listen: tls material changed
    ansible.builtin.command: /opt/bin/purge-cache.sh

Handlers in a role live in roles/<name>/handlers/main.yml and are visible to the whole play, so one role's task can notify another role's handler. That is powerful and occasionally surprising - a good reason to prefix handler names and topics with the role name.

A final ordering subtlety: flush_handlers flushes all pending handlers, not just the one you had in mind. If the play has accumulated several notifications, they all run at that point. Usually fine; worth knowing before you place a flush in the middle of a delicate sequence.

Tip: Prefix handler names and listen topics with the role name - webapp reload, not reload. Handler namespaces are shared across the whole play.
Goal: Put this to work in the ansible-role-refactor lab. Open /labs/ansible, pick ansible-role-refactor, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.