Ansible / Editorial solution guide

Diagnose perpetual changes and repeated handlers

Separate unstable desired content, imperative commands, reported change, handler scheduling, and real application activation in a synthetic diagnosis.

About 4 min readContent reviewed

Read-only knowledge base · No public comments

Recognize the symptoms

A second run reports changed again and a handler is repeatedly notified even though the intended configuration seems unchanged. Alternatively, real configuration differences occur but no activation is reported. Preserve the relevant task result, module arguments, input sources, handler identity, and the first run’s actual postcondition before suppressing output or forcing a flag.

Understand the likely causes

  • Volatile template data, non-deterministic ordering, an imperative command, incorrect changed_when, destination metadata differences, or genuine external drift can explain repeated change. A handler can also be misunderstood because definition order, deduplication, flush boundaries, or later failure differ from the author’s expectations. A green summary is not evidence of true idempotence.

1. Separate reported change from the actual state delta

Identify which exact task reports changed and which documented module comparison produced that result. Compare intended content and relevant ownership/mode metadata using synthetic or authorized redacted evidence. Do not start by setting changed_when: false, because it changes reporting without preventing effects. A read-only command can need an explicit truthful reporting rule, while a state module should normally retain its documented semantics. Also check whether the second run really used the same inputs, dependency versions, target state, and template source as the first.

2. Inspect unstable desired inputs

A generated timestamp, random value, environment-dependent ordering, or changing discovery output can make every rendered file different. Sort only when ordering is semantically irrelevant, and remove volatility only when it is not an application requirement. Preserve exact newlines and serialization rules. The offline fixture below shows how a run marker can change text while a stable desired configuration remains equal. It is not a Jinja render or a comparison against a real target file, so the result narrows a model rather than proving the deployed module is wrong.

python Compare volatile and stable synthetic desired text
first = 'mode=review\nrun=1\n'
second = 'mode=review\nrun=2\n'
stable_first = 'mode=review\n'
stable_second = 'mode=review\n'
print('volatile_changed=' + str(first != second))
print('stable_changed=' + str(stable_first != stable_second))
Expected output
volatile_changed=True
stable_changed=False

Only in-memory strings are compared. This confirms the declared text contract, not template-module idempotence, file metadata, or application health.

3. Trace notification through its real handler lifecycle

A changed task queues notification; it does not necessarily execute the handler immediately. Several notifications can deduplicate to one handler execution per host at the relevant flush, and definition order differs from notification order. Later task failures or unreachable state can affect activation according to the chosen policy. Record the actual handler insertion context, including roles and includes. A repeated notification can be correct when state truly changed, while an unchanged report can wrongly suppress required activation. Keep these cases distinct rather than treating every restart-like symptom as one handler bug.

4. Restore deterministic state and truthful reporting

Prepare a minimal correction in a disposable project copy: stable template inputs, a purpose-built module, justified command guards, or a corrected change predicate. Preserve configuration validation before activation and application verification afterward. The following non-runnable example shows notification structure with a fictional template; parsing does not execute it. Never use forced unchanged status merely to satisfy a second-run test, and do not accept a handler’s successful result as proof of service correctness. Retain a rollback plan for any later authorized target change.

yaml Non-runnable template and handler correction design
- name: Illustrate truthful change-to-handler wiring
  hosts: lab_nodes
  gather_facts: false
  tasks:
    - name: Describe a deterministic template deployment
      ansible.builtin.template:
        src: report.conf.j2
        dest: /srv/synthetic-report/report.conf
        mode: '0640'
      notify: Describe activation
  handlers:
    - name: Describe activation
      ansible.builtin.debug:
        msg: Synthetic activation evidence only

The template source is a fictional lab artifact. No rendering, copy, notification, handler, service, or target operation was executed.

Verify the fix

In an authorized disposable execution test, prove first convergence, stable second-run reporting, required activation once, no unnecessary activation on unchanged input, and correct application behavior. Include real drift and partial-failure cases. For this course, report only YAML and offline string-model checks; engine and target idempotence remain untested.

Keep it from recurring

  • Keep generated content deterministic, document module/change semantics, and retain exact rendered-output fixtures where possible. Review timestamps, random inputs, collection upgrades, metadata, and handler insertion changes. Test truthful change behavior rather than green summaries, and keep convergence, activation, and application acceptance as separate release gates.

Official references