<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>reliability on Samirtech</title>
    <link>https://samirtech.org/tags/reliability/</link>
    <description>Recent content in reliability on Samirtech</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Thu, 24 Sep 2026 00:00:00 +0100</lastBuildDate><atom:link href="https://samirtech.org/tags/reliability/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Monitoring the Monitor with Atomic Status Files</title>
      <link>https://samirtech.org/posts/monitoring-the-monitor-with-atomic-status-files/</link>
      <pubDate>Thu, 24 Sep 2026 00:00:00 +0100</pubDate>
      
      <guid>https://samirtech.org/posts/monitoring-the-monitor-with-atomic-status-files/</guid>
      <description>A monitoring script can be running on schedule and still leave you with no trustworthy monitoring data.
I hit a simple example: the process that generated a JSON health report started correctly, but the published report was zero bytes. Anything reading that file either failed to parse it or had no state to display. The scheduler was healthy. The monitoring output was not.
The cause was a common shell pattern:</description>
      <content>&lt;p&gt;A monitoring script can be running on schedule and still leave you with no trustworthy monitoring data.&lt;/p&gt;
&lt;p&gt;I hit a simple example: the process that generated a JSON health report started correctly, but the published report was zero bytes. Anything reading that file either failed to parse it or had no state to display. The scheduler was healthy. The monitoring output was not.&lt;/p&gt;
&lt;p&gt;The cause was a common shell pattern:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;health-report &amp;gt; last-report.json
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The shell opens &lt;code&gt;last-report.json&lt;/code&gt; and truncates it &lt;em&gt;before&lt;/em&gt; &lt;code&gt;health-report&lt;/code&gt; starts. If the command crashes, is killed or fails while writing, the previous valid report is already gone.&lt;/p&gt;
&lt;h2 id=&#34;the-final-filename-is-an-interface&#34;&gt;The final filename is an interface&lt;/h2&gt;
&lt;p&gt;A status file is not merely a log. Other programs treat its name as an interface: dashboards read it, alerting scripts parse it and people use it during incidents.&lt;/p&gt;
&lt;p&gt;That means readers should see one of two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the complete previous report; or&lt;/li&gt;
&lt;li&gt;the complete new report.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;They should never see a half-written transition between them.&lt;/p&gt;
&lt;p&gt;The standard fix is to write beside the final file and rename only after validation:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;set -eu
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;state_dir&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$HOME&lt;span style=&#34;color:#e6db74&#34;&gt;/.local/state/infra-health&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;final&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$state_dir&lt;span style=&#34;color:#e6db74&#34;&gt;/last-report.json&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;tmp&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;$(&lt;/span&gt;mktemp &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$state_dir&lt;span style=&#34;color:#e6db74&#34;&gt;/.last-report.XXXXXX&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;trap &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;rm -f &amp;#34;$tmp&amp;#34;&amp;#39;&lt;/span&gt; EXIT
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;health-report &amp;gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$tmp&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;test -s &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$tmp&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python3 -m json.tool &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$tmp&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt; &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;chmod &lt;span style=&#34;color:#ae81ff&#34;&gt;600&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$tmp&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mv -f &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$tmp&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$final&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;trap - EXIT
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On the same filesystem, &lt;code&gt;rename&lt;/code&gt; is atomic. Readers see the old inode until the new one replaces it; they do not observe a partially written report.&lt;/p&gt;
&lt;p&gt;Creating the temporary file in the destination directory is important. A move across filesystems may fall back to copy-and-delete semantics and lose the atomic guarantee.&lt;/p&gt;
&lt;p&gt;Atomic publication has two important limits. First, rename atomicity does not itself guarantee crash durability. A power loss can still discard data that existed only in cache; code that requires that guarantee must flush and &lt;code&gt;fsync&lt;/code&gt; the temporary file before renaming it, then &lt;code&gt;fsync&lt;/code&gt; the containing directory, while accounting for filesystem-specific behaviour. Second, atomic rename does not serialize concurrent writers. Two jobs can each publish a complete file and the last rename wins, even if it contains older data. I use one writer, a lock or generation checks when runs can overlap; unique temporary names prevent temporary-file collisions but not this race.&lt;/p&gt;
&lt;h2 id=&#34;validate-meaning-not-only-syntax&#34;&gt;Validate meaning, not only syntax&lt;/h2&gt;
&lt;p&gt;Non-empty valid JSON is the minimum gate, not always the complete one. A report can be syntactically valid and operationally useless.&lt;/p&gt;
&lt;p&gt;For a richer report I also validate required fields:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; json
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; pathlib &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; Path
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;p &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Path(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;candidate.json&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;loads(p&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;read_text())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;required &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;generated_at&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;overall_state&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;checks&amp;#34;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;missing &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; required &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; data&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;keys()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; missing:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SystemExit&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;missing fields: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;sorted(missing)&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; isinstance(data[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;checks&amp;#34;&lt;/span&gt;], list):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SystemExit&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;checks must be a list&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The producer should exit non-zero when it cannot produce a complete report. A wrapper can then preserve the last good file and alert on the failed attempt.&lt;/p&gt;
&lt;h2 id=&#34;keep-generation-state-separate-from-observed-state&#34;&gt;Keep generation state separate from observed state&lt;/h2&gt;
&lt;p&gt;The report tells me what the checks found. It should not be the only evidence that report generation itself is working.&lt;/p&gt;
&lt;p&gt;I track three independent signals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;content:&lt;/strong&gt; the latest valid report says healthy or unhealthy;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;freshness:&lt;/strong&gt; its generation time is recent enough for the schedule;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;execution:&lt;/strong&gt; the scheduler last ran the producer and recorded its exit status.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A healthy but stale file is a monitoring failure. So is a fresh file that omits required checks. And a failed producer should not replace yesterday&amp;rsquo;s useful report with an empty one.&lt;/p&gt;
&lt;p&gt;A simple freshness probe might look like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; pathlib &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; Path
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; time
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;report &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Path(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;last-report.json&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;max_age &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;60&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;age &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; time&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;time() &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; report&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;stat()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;st_mtime
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; age &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; max_age:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SystemExit&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;report is stale: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;age&lt;span style=&#34;color:#e6db74&#34;&gt;:&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;.0f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;s&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The threshold must match the real schedule and allow a sensible grace period. A five-minute freshness threshold for an hourly job creates noise, while a ten-day threshold for a daily job hides failures.&lt;/p&gt;
&lt;h2 id=&#34;do-not-destroy-the-evidence-on-failure&#34;&gt;Do not destroy the evidence on failure&lt;/h2&gt;
&lt;p&gt;When generation fails, I keep both kinds of evidence:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the previous valid report remains at the stable path;&lt;/li&gt;
&lt;li&gt;a small failure record captures when the attempt failed and the command&amp;rsquo;s exit status.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I do not copy raw command output into alerts by default. Health commands can accidentally expose paths, hostnames or credentials. A concise allowlisted summary is safer, with detailed logs retained privately and with bounded size.&lt;/p&gt;
&lt;p&gt;The wrapper can also record the failure atomically:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; ! health-report &amp;gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$tmp&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;; &lt;span style=&#34;color:#66d9ef&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    printf &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;{&amp;#34;failed_at&amp;#34;:&amp;#34;%s&amp;#34;,&amp;#34;exit&amp;#34;:1}\n&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;$(&lt;/span&gt;date --iso-8601&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;seconds&lt;span style=&#34;color:#66d9ef&#34;&gt;)&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;        &amp;gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$state_dir&lt;span style=&#34;color:#e6db74&#34;&gt;/.last-failure.tmp&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    mv -f &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$state_dir&lt;span style=&#34;color:#e6db74&#34;&gt;/.last-failure.tmp&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$state_dir&lt;span style=&#34;color:#e6db74&#34;&gt;/last-failure.json&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    exit &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In production I pass through the real exit status and validate this JSON too. The shortened example shows the separation: failed attempt metadata does not overwrite the last known-good health report.&lt;/p&gt;
&lt;h2 id=&#34;test-the-failure-path-deliberately&#34;&gt;Test the failure path deliberately&lt;/h2&gt;
&lt;p&gt;A success-only test misses the problem this design is meant to solve. I test at least four cases in a scratch directory:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the producer writes valid JSON and the final file changes;&lt;/li&gt;
&lt;li&gt;the producer exits non-zero and the prior report remains byte-for-byte identical;&lt;/li&gt;
&lt;li&gt;the producer writes empty output and validation rejects it;&lt;/li&gt;
&lt;li&gt;the producer writes malformed JSON and validation rejects it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I also run a reader repeatedly while replacing the file, checking that it only ever parses the old or new complete document.&lt;/p&gt;
&lt;p&gt;Finally, I let the real scheduler run once and confirm the final file&amp;rsquo;s timestamp and content changed. A manual run proves the script; a scheduled run proves the installed path, environment and permissions.&lt;/p&gt;
&lt;h2 id=&#34;the-rule-i-now-use&#34;&gt;The rule I now use&lt;/h2&gt;
&lt;p&gt;Anything consumed under a stable filename is published data. Generate it privately, validate it, then replace it atomically.&lt;/p&gt;
&lt;p&gt;That rule applies to health reports, backup manifests, checksums, generated configuration, feed caches and dashboard data. The implementation is small, but it changes a monitoring system from “the command probably ran” into an interface that fails without destroying its last useful answer.&lt;/p&gt;
</content>
    </item>
    
  </channel>
</rss>
