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.
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.
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))
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.
- 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.