<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: lanony82</title>
    <description>The latest articles on DEV Community by lanony82 (@lanony82).</description>
    <link>https://dev.to/lanony82</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4070649%2F6570140f-2a9e-422b-a915-498bd588e93d.jpg</url>
      <title>DEV Community: lanony82</title>
      <link>https://dev.to/lanony82</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lanony82"/>
    <language>en</language>
    <item>
      <title>Four Sanity Patterns for Distributed-System Testing</title>
      <dc:creator>lanony82</dc:creator>
      <pubDate>Mon, 10 Aug 2026 06:42:38 +0000</pubDate>
      <link>https://dev.to/lanony82/four-sanity-patterns-for-distributed-system-testing-395l</link>
      <guid>https://dev.to/lanony82/four-sanity-patterns-for-distributed-system-testing-395l</guid>
      <description>&lt;p&gt;Four traps I've walked into (and out of) while writing fault-injection tests&lt;br&gt;
against a distributed system. Each one produced a &lt;strong&gt;vacuous pass or a false&lt;br&gt;
diagnosis&lt;/strong&gt; — the test looked fine, or the log looked damning, but the&lt;br&gt;
reality was the opposite. Each has a durable escape recipe.&lt;/p&gt;

&lt;p&gt;They all share one shape: &lt;strong&gt;you think you're observing X, but you're&lt;br&gt;
observing Y.&lt;/strong&gt; The escape is always to add a second, independent channel&lt;br&gt;
of observation before trusting the first.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Cumulative-log fallacy
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trap.&lt;/strong&gt; A long-running daemon appends to the same log file across many&lt;br&gt;
test iterations. Your test greps the file for a marker&lt;br&gt;
(&lt;code&gt;"parent died"&lt;/code&gt;, &lt;code&gt;"failed to init"&lt;/code&gt;, &lt;code&gt;"shutting down"&lt;/code&gt;). The grep hits —&lt;br&gt;
but the hit is from a &lt;strong&gt;prior teardown&lt;/strong&gt;, not the current run. Read as&lt;br&gt;
current state, it produces a false diagnosis with high confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concrete failure I hit.&lt;/strong&gt; Testing a scheduler daemon on a shared host:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Test 1 teardown calls the shutdown helper. Scheduler logs
&lt;code&gt;"parent has died"&lt;/code&gt; on the way out. That line is now permanent.&lt;/li&gt;
&lt;li&gt;Test 2 setup starts a fresh scheduler. It's alive and healthy.&lt;/li&gt;
&lt;li&gt;My test tails the log, sees &lt;code&gt;"parent died"&lt;/code&gt;, concludes
&lt;em&gt;"scheduler exits during init — must be transient on this build."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;I &lt;code&gt;xfail&lt;/code&gt; five test cases with the wrong premise and file a comment
asking dev what changed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A reviewer opened the same log, spotted the shutdown call &lt;strong&gt;immediately&lt;br&gt;
above&lt;/strong&gt; the &lt;code&gt;parent died&lt;/code&gt; line, and pointed out the message was from&lt;br&gt;
teardown, not natural exit. Scheduler was daemon-mode the whole time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause.&lt;/strong&gt; &lt;code&gt;grep&lt;/code&gt; has no time axis. The signal&lt;br&gt;
(&lt;code&gt;"parent died"&lt;/code&gt;) truly means "the scheduler exited" — but the&lt;br&gt;
&lt;strong&gt;timestamp&lt;/strong&gt; matters more than the text, and grep ignores it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escape recipes&lt;/strong&gt; — pick the cheapest that fits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nonce&lt;/strong&gt;: write a &lt;code&gt;uuid4().hex&lt;/code&gt; somewhere the daemon echoes on startup
(config file, env-var-driven log marker). Grep starts at the line
containing the nonce.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Byte offset&lt;/strong&gt;: snapshot &lt;code&gt;os.stat(log).st_size&lt;/code&gt; before the action;
grep only &lt;code&gt;content[offset:]&lt;/code&gt;. Caveat: brittle if the daemon rotates
the log or opens a new one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Truncate&lt;/strong&gt;: rotate or truncate the log at fixture setup. Cheapest of
all when the log is per-test-run and not a durable audit trail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The rule.&lt;/strong&gt; &lt;em&gt;Never &lt;code&gt;grep &amp;lt;pattern&amp;gt; &amp;lt;log&amp;gt;&lt;/code&gt; without a since-when anchor&lt;br&gt;
when the fixture restarts the daemon.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Companion checks&lt;/strong&gt; before publishing a "the daemon did X" claim:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is the line I'm quoting from &lt;em&gt;this&lt;/em&gt; run? (log timestamp vs fixture clock)&lt;/li&gt;
&lt;li&gt;Does the live process table agree with the log? (alive PID &lt;em&gt;and&lt;/em&gt; recent line)&lt;/li&gt;
&lt;li&gt;Did anything in the fixture issue a stop between the action and the observation?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Any "no / don't know" → cumulative-log risk.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Silent filter miss
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trap.&lt;/strong&gt; Your test predicate — &lt;code&gt;_pgrep&lt;/code&gt;, &lt;code&gt;_find_child&lt;/code&gt;, &lt;code&gt;is_x_running&lt;/code&gt; —&lt;br&gt;
returns empty. There are two indistinguishable causes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The target really isn't there.&lt;/li&gt;
&lt;li&gt;Your filter is wrong; the target IS there but you missed it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cause (2) produces no error, only &lt;em&gt;silence&lt;/em&gt;. You can chase ghost theories&lt;br&gt;
about (1) for hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concrete failure I hit.&lt;/strong&gt; My predicate was&lt;br&gt;
&lt;code&gt;pgrep -f $INSTALL_ROOT -a&lt;/code&gt; looking for the scheduler binary path. On the&lt;br&gt;
build I was testing, the scheduler's argv was bare&lt;br&gt;
&lt;code&gt;scheduler --writefd 6&lt;/code&gt; — no install-root prefix, because it was&lt;br&gt;
fork-exec'd from the parent with a constructed argv. Filter returned&lt;br&gt;
empty every time.&lt;/p&gt;

&lt;p&gt;I built a story to explain the silence: &lt;em&gt;"scheduler is transient on this&lt;br&gt;
build, exits during init, that's why I never see it."&lt;/em&gt; Wrote a comment&lt;br&gt;
to dev, &lt;code&gt;xfail&lt;/code&gt;'d five tests, updated memory. All wrong.&lt;/p&gt;

&lt;p&gt;What broke the loop was running the same query outside my code:&lt;br&gt;
&lt;code&gt;pgrep -af scheduler&lt;/code&gt; in the shell — with no install-root gate —&lt;br&gt;
returned a live PID with &lt;code&gt;scheduler --writefd 6&lt;/code&gt; in the cmdline. Alive&lt;br&gt;
the whole time. &lt;strong&gt;My filter was the bug.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause.&lt;/strong&gt; A predicate with one overly-strict gate returns empty&lt;br&gt;
identically to a truly-absent target. Absence claims from filters are&lt;br&gt;
vacuous — until you cross-check them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escape recipes.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Independent observation&lt;/strong&gt; — verify absence with a channel that does
NOT go through the same filter your code uses. &lt;code&gt;ps -eo pid,cmd&lt;/code&gt;, &lt;code&gt;ls
-l /proc/*/exe&lt;/code&gt;, &lt;code&gt;ss -lntp&lt;/code&gt;, direct log tail. Different axis, no
shared gate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Introspect the filter axis&lt;/strong&gt; — if your code matches by argv, print
the target's actual argv first. Fork-exec'd children often have
constructor-built argv. systemd/launchd services often set &lt;code&gt;comm&lt;/code&gt; but
not &lt;code&gt;argv&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test each gate independently&lt;/strong&gt; — if the filter has 3 gates
(&lt;code&gt;--user=X --root=Y --name=Z&lt;/code&gt;), verify each in isolation. A single
wrong gate kills the match.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The smell signal.&lt;/strong&gt; If you catch yourself constructing a story to&lt;br&gt;
explain a &lt;strong&gt;persistent, deterministic&lt;/strong&gt; empty result, the story is&lt;br&gt;
probably wrong. Real intermittent absence is racy / time-dependent; a&lt;br&gt;
filter bug is deterministic. &lt;strong&gt;Deterministic absence + plausible&lt;br&gt;
explanation = check the filter, not the story.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Polling-fire window
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trap.&lt;/strong&gt; You're testing a scheduler (or any polling-config system) that&lt;br&gt;
re-reads its config every N seconds. You install a test entry whose&lt;br&gt;
fire time is &lt;em&gt;before&lt;/em&gt; the next poll boundary. When the scheduler&lt;br&gt;
finally polls, the entry looks past-due, and the "past entries wait&lt;br&gt;
until next day" rule kicks in. The entry never runs in your test&lt;br&gt;
window. &lt;strong&gt;There is no error log.&lt;/strong&gt; The install lines look identical to&lt;br&gt;
a normal case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concrete failure I hit.&lt;/strong&gt; Scheduler's config-refresh poll interval was&lt;br&gt;
60 s. My test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;fire_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;  &lt;span class="c1"&gt;# 10 seconds from wall-clock
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Timeline:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;T (s)&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;test writes a job entry &lt;code&gt;fire_at = T+10 -execute /bin/sleep 90; touch X&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;~50&lt;/td&gt;
&lt;td&gt;scheduler polls, sees new entry; fire time (T+10) is 40s in the past → "wait until tomorrow"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10..end&lt;/td&gt;
&lt;td&gt;my &lt;code&gt;_wait(...)&lt;/code&gt; loops on the marker file. Times out.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The scheduler log even looked healthy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Adding job at 20:15:52 -execute /bin/sh -c '/bin/sleep 90; ...'
job 0 fire=72952 type=EXECUTE ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading those, I thought the job was queued. It &lt;strong&gt;was&lt;/strong&gt; queued — for&lt;br&gt;
tomorrow (72952 seconds since midnight = 20:15:52 the next day).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause.&lt;/strong&gt; Polling introduces a hidden discrete-time boundary&lt;br&gt;
between "install" and "first evaluation". Any entry whose fire time&lt;br&gt;
falls inside that boundary is silently rerouted through the past-time&lt;br&gt;
branch. The branch has no log signal for "this entry got rescheduled&lt;br&gt;
because you installed it too late."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escape recipes.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# WRONG — racy with the polling boundary
&lt;/span&gt;&lt;span class="n"&gt;fire_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

&lt;span class="c1"&gt;# RIGHT — guaranteed to fall in the FIRST polling window after install
&lt;/span&gt;&lt;span class="n"&gt;fire_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;       &lt;span class="c1"&gt;# slack ≥ 30s to absorb fixture timing
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For wait timeouts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Cover: next poll (≤ N) + fire delay (N + slack) + job runtime
&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;job_runtime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Extra guard.&lt;/strong&gt; After writing the config, assert the fire time is in&lt;br&gt;
the future &lt;em&gt;from the scheduler's perspective&lt;/em&gt; (i.e. from a fresh&lt;br&gt;
&lt;code&gt;date&lt;/code&gt; on the target host, not from the test-runner's clock). A&lt;br&gt;
one-line guard catches misconfiguration before the wait swallows it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where else this bites.&lt;/strong&gt; Cron-like systems with poll interval, DB-&lt;br&gt;
mtime-polled app-server config, file-watcher debouncers, etcd/K8s&lt;br&gt;
watch-with-resync intervals, distributed job schedulers using&lt;br&gt;
mtime-tick to detect config changes. Whenever you see "polling&lt;br&gt;
interval = N" in the spec, your test inputs need &lt;code&gt;&amp;gt; N + slack&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note.&lt;/strong&gt; This is a &lt;em&gt;special case&lt;/em&gt; of pattern 2 — the "filter" is the&lt;br&gt;
past-time gate, the empty result is "marker never appears", and the&lt;br&gt;
fix is the same: verify with an independent observation&lt;br&gt;
(&lt;code&gt;ssh + tail&lt;/code&gt; of the scheduler's own log) before trusting your test&lt;br&gt;
predicate's silence.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Monotonic identity
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trap.&lt;/strong&gt; You're writing a fault-injection or recovery test. You need to&lt;br&gt;
assert that after the injection, the system "recovered to the same&lt;br&gt;
state." You reach for the identifier that's easiest to observe — PID,&lt;br&gt;
listening port, cmdline, IP:port four-tuple, process count. Test&lt;br&gt;
passes. Test is &lt;em&gt;vacuous&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concrete failure I hit.&lt;/strong&gt; A session-preservation test on a session&lt;br&gt;
manager. Fault: burst-reset all its client sockets. Post-fault gate:&lt;br&gt;
"count of active sessions unchanged."&lt;/p&gt;

&lt;p&gt;Passed. Repeatedly. Then a review pass caught that the session manager&lt;br&gt;
was &lt;strong&gt;keeping session-id rows in its table long after&lt;/strong&gt; the owning TCP&lt;br&gt;
socket died — a ~90-second batch-sweeper cycle, not per-session TTL.&lt;br&gt;
So during the fault, sessions were logically dead (the client gRPC&lt;br&gt;
stream was broken, the next request would be rejected as "invalid&lt;br&gt;
session id"), but the &lt;em&gt;count&lt;/em&gt; stayed the same because the rows hadn't&lt;br&gt;
been reaped yet. A user staring at the UI got locked out. My test said&lt;br&gt;
green.&lt;/p&gt;

&lt;p&gt;Fix: swap the count-based gate for &lt;strong&gt;session-id set preservation&lt;/strong&gt; —&lt;br&gt;
snapshot the baseline set of session ids before the fault, assert the&lt;br&gt;
post-fault set is a superset. Session ids are never reused, so set&lt;br&gt;
membership = identity preservation. Every fault-injection test in the&lt;br&gt;
suite grew this gate. Several latent bugs that had been passing for&lt;br&gt;
months surfaced immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause.&lt;/strong&gt; Two different kinds of identifier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spatial&lt;/strong&gt; — names &lt;em&gt;what's there now&lt;/em&gt;. PID, IP, listening port,
four-tuple, cmdline, file path, resource count. Reused: the same
value can name entity X at t1 and entity Y at t2. Describes a
snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporal / monotonic&lt;/strong&gt; — names &lt;em&gt;which event&lt;/em&gt;. session id, epoch,
term, generation, LSN, offset, fencing token, revision. Never
reused. Describes a moment in history.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recovery tests must assert &lt;strong&gt;across time&lt;/strong&gt; ("post state contains&lt;br&gt;
baseline state"). Spatial identifiers can't carry that assertion —&lt;br&gt;
they only describe &lt;em&gt;now&lt;/em&gt;. Monotonic identifiers can.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How spatial identifiers betray you.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PID reuse&lt;/strong&gt; — parent dies, restarts; new PID can be smaller than
old (PID counter wrap). Fork may hit a released PID with identical
cmdline. &lt;code&gt;pid_changed&lt;/code&gt; and &lt;code&gt;pid_same&lt;/code&gt; both lie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port-listen ≠ working&lt;/strong&gt; — a service binds its socket the moment
&lt;code&gt;listen()&lt;/code&gt; returns, but internal state (open segments, DB attach,
cache warmup) can be minutes behind. &lt;code&gt;ss -lnt&lt;/code&gt; says green; next
request rejected. The classic "startup completed = healthy" trap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cmdline is just a string&lt;/strong&gt; — parent and child service managers
often share exact cmdline. Only PPID or start time disambiguates.
Falling back to "the PID with smaller start time" is fragile under
restart.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The monotonic-identity zoo&lt;/strong&gt; (patterns to recognize across systems):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Identifier&lt;/th&gt;
&lt;th&gt;Allocator&lt;/th&gt;
&lt;th&gt;Where it lives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;session id&lt;/td&gt;
&lt;td&gt;server at login&lt;/td&gt;
&lt;td&gt;web sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;epoch / term&lt;/td&gt;
&lt;td&gt;leader election&lt;/td&gt;
&lt;td&gt;Raft, Kafka controller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;generation number&lt;/td&gt;
&lt;td&gt;restart event&lt;/td&gt;
&lt;td&gt;GFS chunk version, ZK zxid high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LSN / offset&lt;/td&gt;
&lt;td&gt;per write&lt;/td&gt;
&lt;td&gt;Postgres WAL, Kafka partition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fencing token&lt;/td&gt;
&lt;td&gt;lock service&lt;/td&gt;
&lt;td&gt;Chubby, etcd lease&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;logical clock&lt;/td&gt;
&lt;td&gt;per event&lt;/td&gt;
&lt;td&gt;Lamport, vector clock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;revision / version&lt;/td&gt;
&lt;td&gt;watch cursor&lt;/td&gt;
&lt;td&gt;etcd revision, ZK zxid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Common shape: &lt;strong&gt;monotonic + never reused&lt;/strong&gt;. Distinguishing epoch 5 from&lt;br&gt;
epoch 6 needs no other context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three reflex questions.&lt;/strong&gt; When reviewing recovery / reconnect /&lt;br&gt;
failover / idempotency code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What identity is being used to mean "same thing"?&lt;/li&gt;
&lt;li&gt;Can it be reused? Even in a narrow window?&lt;/li&gt;
&lt;li&gt;If the peer restarted / network flapped / clock jumped, does the
judgement still hold? "Depends on luck" → upgrade to monotonic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Where this transfers.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reconnect protocols&lt;/strong&gt; — &lt;code&gt;client_id&lt;/code&gt; (monotonic) survives IP
changes; four-tuple loses identity every reconnect. Stripe's
idempotency key is the same idea.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leader election&lt;/strong&gt; — Raft term is the textbook. Without it,
split-brain detection by hostname is unsafe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distributed locks&lt;/strong&gt; — Kleppmann's &lt;em&gt;How to do distributed locking&lt;/em&gt;
attacks Redlock exactly because it lacks fencing tokens: a
GC-paused holder writes with a stale token → storage rejects → safe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DB replication&lt;/strong&gt; — Postgres LSN, MySQL GTID. "Replicated through
transaction #1000" lies on retry; "through LSN=X" doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message queue consumers&lt;/strong&gt; — Kafka offset commits are monotonic;
cursorless long-polling drops events on reconnect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch APIs&lt;/strong&gt; — etcd/ZK watch resume by revision; without the
cursor, events between disconnect and reconnect vanish silently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Implementation pattern.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Spatial (fragile)
&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;post_pid&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;baseline_pid&lt;/span&gt;

&lt;span class="c1"&gt;# Temporal (sound)
&lt;/span&gt;&lt;span class="n"&gt;baseline_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;snapshot_session_ids&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;inject_fault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;assert_ids_preserved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseline_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The unifying shape
&lt;/h2&gt;

&lt;p&gt;All four are variations on &lt;strong&gt;"my observation channel is lying to me,&lt;br&gt;
and I don't know it."&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;What lies&lt;/th&gt;
&lt;th&gt;Independent check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cumulative log&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;grep&lt;/code&gt; reads history as state&lt;/td&gt;
&lt;td&gt;Nonce / offset / truncate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Silent filter miss&lt;/td&gt;
&lt;td&gt;Filter empty ≠ target absent&lt;/td&gt;
&lt;td&gt;Second observation channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polling-fire window&lt;/td&gt;
&lt;td&gt;Install-log looks successful&lt;/td&gt;
&lt;td&gt;Assert fire time is future, from target's clock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monotonic identity&lt;/td&gt;
&lt;td&gt;Spatial ids look preserved&lt;/td&gt;
&lt;td&gt;Set-membership of never-reused ids&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The escape is the same shape every time: &lt;strong&gt;before publishing a claim,&lt;br&gt;
have a second independent channel confirm it.&lt;/strong&gt; Different query axis,&lt;br&gt;
different tool, different observer. Two channels agree → high&lt;br&gt;
confidence. One channel and a plausible story → almost certainly&lt;br&gt;
wrong.&lt;/p&gt;

&lt;p&gt;This is the QA-side version of "don't trust a single monitor": the&lt;br&gt;
same discipline SREs apply to production observability, applied to&lt;br&gt;
test observability.&lt;/p&gt;




&lt;h2&gt;
  
  
  Companion practice: self-question before archiving
&lt;/h2&gt;

&lt;p&gt;The four patterns above are trigger-specific. There's a fifth,&lt;br&gt;
higher-level habit that catches the same class of errors when you&lt;br&gt;
don't know which trigger applies:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before archiving any evidence-heavy result — a baseline, a&lt;br&gt;
diagnosis, a "this test suite is complete" claim — spend 15–30&lt;br&gt;
minutes on a structured skeptical pass.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For every load-bearing claim in the write-up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Name the specific query that would break it.&lt;/strong&gt; Off-by-one in a
range filter. Aggregation granularity too coarse. Missing a source.
Cherry-picked window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-bucket log evidence at the granularity the claim needs.&lt;/strong&gt;
"Zero errors during steady state" is not verifiable at &lt;em&gt;minute&lt;/em&gt;
granularity if the run ends mid-minute; bucket at &lt;em&gt;seconds&lt;/em&gt; around
the transition points.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-reference independent data sources.&lt;/strong&gt; Client-side failures
vs server-side errors vs downstream cleanup counts — three views of
the same event; if they disagree, the headline claim is soft.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When a claim survives, keep it. When it doesn't, downgrade it in
the archive itself.&lt;/strong&gt; "No leak" → "no leak signal at 200 users; 400
users +9 fd in final 2 min inconclusive." Downgrades live in the
evidence, not in a follow-up note.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A recent 30-minute pass on a performance baseline caught three real&lt;br&gt;
problems using exactly this loop: an "fd flat" claim that was true at&lt;br&gt;
one load level but not at another, a "reaper cleaned N orphans" claim&lt;br&gt;
where the reaped set had zero overlap with the failed set, and a "N&lt;br&gt;
server errors = mix of noise + shutdown race" claim where the&lt;br&gt;
uptime-conditioned noise source couldn't have produced any of them.&lt;/p&gt;

&lt;p&gt;None of those would have surfaced from re-reading the summary. They&lt;br&gt;
came from re-running the queries at tighter buckets and against&lt;br&gt;
cohorts the summary hadn't cross-checked.&lt;/p&gt;

&lt;p&gt;The habit is cheap and catches errors that would otherwise ship.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>distributedsystems</category>
      <category>qa</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
