RHCE study materials
RHCE study resources

Handlers Firing Every Run and a Conditional That Never Triggers

RHCE · Put your knowledge to work in context.

All case studies

Handlers Firing Every Run and a Conditional That Never Triggers

A playbook manages 30 app servers. It runs command: /usr/bin/appctl status registered as appstat, then a debug task gated by when: appstat.stdout > 100 that never fires even though the reported value is 250 on several hosts. A second task, shell: grep -c worker /etc/app/app.conf, always reports changed, so the restart app handler fires on every run, producing 30 restarts per deploy. A template task inside a block guarded by when: ansible_facts['distribution_major_version'] == '9' notifies the handler on only some hosts. The team requires: no restart unless configuration content actually changed, a failing config validation must abort before any restart, and a hard failure when the worker count is below 100.

1. Why does `when: appstat.stdout > 100` not do what the author intended, and how do you turn the worker-count requirement into a hard failure?

Read the worked answer

Registered stdout is always a string. Comparing it to the integer 100 is a type mismatch, so the conditional raises a templating error rather than quietly evaluating to false - and a run that resolves the value as 250 yet never fires usually means the error is being masked or the value carries whitespace/newline. The fix is a cast plus an explicit type check, because | int silently returns 0 for non-numeric input and 0 will also fail the threshold for the wrong reason:

- name: Read worker count
  ansible.builtin.command: /usr/bin/appctl status
  register: appstat
  changed_when: false

- name: Validate worker count
  ansible.builtin.assert:
    that:
      - appstat.stdout | trim is match('^[0-9]+$')
      - appstat.stdout | int >= 100
    fail_msg: "worker count '{{ appstat.stdout | trim }}' invalid on {{ inventory_hostname }}"

Using assert rather than when makes the requirement fail the play loudly. changed_when: false is mandatory on a status command, otherwise it is permanently changed and notifies handlers.

2. Why does the `restart app` handler fire on every run, and how do you make restart conditional on real content change?

Read the worked answer

shell and command have no way to know the remote state; Ansible marks them changed whenever they run and return success. grep -c also exits non-zero when the count is 0, so it can fail outright. Hence a notify is triggered every deploy.

Ranked fixes:

  1. Replace the read with a state-aware module. ansible.builtin.slurp plus an assert, or ansible.builtin.stat, never report changed.
  2. Make the edit idempotent with ansible.builtin.template or lineinfile instead of shelling out. template reports changed only when rendered content differs, which is exactly the notify condition wanted.
  3. If the shell read must stay:
- name: Count workers
  ansible.builtin.command: grep -c worker /etc/app/app.conf
  register: workers
  changed_when: false
  failed_when: false

Then gate the restart explicitly with notify on the template task only, never on the read. Add validate: '/usr/bin/appctl validate %s' to the template task so a syntactically invalid file is rejected before it is written - validation failure then aborts the task, so the handler is not notified and the running service keeps its last good config.

3. The handler is notified only on some hosts for an identical template task. What explains that, and how do you guarantee the restart happens before later tasks run?

Read the worked answer

Two separate effects, both expected Ansible behaviour:

  • The block-level when evaluates per host. On hosts that are not RHEL 9 the task is skipped, and skipped is not changed, so no notify. That is correct - but it means the group is heterogeneous, so verify distribution_major_version against the expected inventory rather than assuming all 30 are RHEL 9.
  • On hosts where the condition is true, the task notifies only if the rendered result differs from the file on disk. Hosts already carrying the target content legitimately do not restart. Confirm with --check --diff before concluding the logic is broken.

Ordering: handlers run at the end of the play by default, so later tasks run against the old config. Insert meta: flush_handlers immediately after the validation and template tasks so the restart is applied before dependent tasks execute:

  • Keep the validate task changed_when: false, failed_when: rc != 0 so validation failure aborts first.
  • If a host fails after notifying, its handlers are discarded unless force_handlers: true is set. For a rolling deploy, also use serial: 5 and max_fail_percentage so the restart boundary is controllable per batch.