<?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: Pratyush Patel</title>
    <description>The latest articles on DEV Community by Pratyush Patel (@pratyush2802).</description>
    <link>https://dev.to/pratyush2802</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4121510%2F32449398-faa2-486b-b928-20c231933e61.gif</url>
      <title>DEV Community: Pratyush Patel</title>
      <link>https://dev.to/pratyush2802</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pratyush2802"/>
    <language>en</language>
    <item>
      <title>Building Faultline: A Reusable Chaos-Injection and Linearizability-Checking Harness</title>
      <dc:creator>Pratyush Patel</dc:creator>
      <pubDate>Fri, 11 Sep 2026 22:40:07 +0000</pubDate>
      <link>https://dev.to/pratyush2802/building-faultline-a-reusable-chaos-injection-and-linearizability-checking-harness-4h6n</link>
      <guid>https://dev.to/pratyush2802/building-faultline-a-reusable-chaos-injection-and-linearizability-checking-harness-4h6n</guid>
      <description>&lt;h1&gt;
  
  
  Building Faultline: A Reusable Chaos-Injection and Linearizability-Checking Harness
&lt;/h1&gt;

&lt;p&gt;Faultline is an open-source Go harness that injects faults into distributed systems, records every concurrent operation clients actually observed, and checks whether that history is linearizable — modeled on the methodology behind Kyle Kingsbury's Jepsen. Its reference target is a three-node etcd cluster; a second, architecturally distinct target (NATS JetStream's key-value store) proves the harness is actually reusable, not just an etcd-specific tool with extra steps.&lt;/p&gt;

&lt;p&gt;No consistency violation was found in either target under the tested conditions. That's reported here as legitimate evidence, not a disappointing result — a rigorous "no violations found across N campaigns, fully reproducible" outcome is real infrastructure-testing work. What makes this worth reading, though, is the six concrete bugs the harness caught &lt;strong&gt;in itself&lt;/strong&gt; while being built — because a correctness tool that has never caught anything isn't credible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
    subgraph cli["cmd/faultline (CLI)"]
        campaign["campaign package\nRunN: seed → schedule + workload, checked"]
    end
    subgraph engine["harness (target-agnostic)"]
        chaos["chaos + chaos/docker\nseeded fault schedule\npartition / kill / delay / drop / reorder\nApply → Verify → Clear"]
        workload["workload package\nconcurrent clients, seeded op mix\nfull invoke/return history"]
        checker["checker package\nWing–Gong search, memoized\nexact numeric equality, deletion-minimal"]
    end
    subgraph contract["client.Client / client.SequentialSpec contract"]
        note["Connect / Invoke / Close · Init / Apply\nsame shape for every target"]
    end
    subgraph targets["target integrations"]
        etcd["targets/etcd\ngRPC, linearizable reads, txn-based CAS"]
        nats["targets/nats\nJetStream KV, revision-guarded CAS"]
        toykv["targets/toykv\nprimary-backup fixture"]
    end
    campaign --&amp;gt; chaos
    campaign --&amp;gt; workload
    workload --&amp;gt; checker
    workload --&amp;gt; contract
    contract --&amp;gt; etcd
    contract --&amp;gt; nats
    contract --&amp;gt; toykv&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;A four-method contract — &lt;code&gt;Connect&lt;/code&gt;/&lt;code&gt;Invoke&lt;/code&gt;/&lt;code&gt;Close&lt;/code&gt;, &lt;code&gt;Init&lt;/code&gt;/&lt;code&gt;Apply&lt;/code&gt; — is the only thing a new target implements. The fault injector, workload generator, and checker are written once and never change when a target is added. Adding NATS required zero changes to any of them.&lt;/p&gt;

&lt;p&gt;Every applied fault is independently &lt;strong&gt;verified&lt;/strong&gt;, never assumed: a partition's &lt;code&gt;Verify&lt;/code&gt; pings across the intended break and asserts the ping fails; a claimed fault only counts as coverage once confirmed against the real container.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checker
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;checker.Check&lt;/code&gt; is a Wing &amp;amp; Gong style search over sequential orderings of a recorded history, memoized on &lt;code&gt;(remaining-operations, sequential-state)&lt;/code&gt;, with a bounded search budget so an unresolvable history reports &lt;strong&gt;inconclusive&lt;/strong&gt; rather than hanging forever. Two things were hardened deliberately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exact numeric equality&lt;/strong&gt; via &lt;code&gt;big.Rat&lt;/code&gt;, recursively across JSON-shaped values — because any client whose wire protocol round-trips numbers through JSON (as ToyKV's HTTP client does) silently turns a Go &lt;code&gt;int&lt;/code&gt; into a &lt;code&gt;float64&lt;/code&gt;, and a naive equality check would flag every such operation as a false violation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collision-safe state caching&lt;/strong&gt; — the memoization key is only a bucket hash; every cache hit is verified with full structural equality before being trusted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before being trusted on any real target, the checker was validated against seven hand-built known-good/known-bad histories. &lt;em&gt;A checker that hasn't been validated proves nothing.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Six bugs the harness found in itself
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ToyKV's CAS wire response used the wrong field.&lt;/strong&gt; The server wrote the CAS outcome to &lt;code&gt;wireResult.OK&lt;/code&gt;; the client read &lt;code&gt;wireResult.Value&lt;/code&gt;. Every real client saw &lt;code&gt;nil&lt;/code&gt; for every CAS regardless of outcome — undetected because unit tests exercised the store directly, never through the HTTP path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;containerIP&lt;/code&gt; only read Docker's legacy default-bridge field.&lt;/strong&gt; &lt;code&gt;.NetworkSettings.IPAddress&lt;/code&gt; is empty for any container on a user-defined network — which every deployment here uses. The partition injector silently resolved an empty peer IP until the first live test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fault injectors bailed on the first cleanup failure.&lt;/strong&gt; &lt;code&gt;Clear()&lt;/code&gt; for partition/kill/netem faults returned immediately on the first error, abandoning cleanup for every other node. This isn't hypothetical — it stranded a live three-node etcd cluster in a partitioned, unrecoverable state for &lt;strong&gt;several days&lt;/strong&gt; before diagnosis. Fixed to be best-effort: attempt every node regardless of earlier failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ping-based verification always "passed."&lt;/strong&gt; The container images didn't ship &lt;code&gt;iputils-ping&lt;/code&gt;, so &lt;code&gt;ping&lt;/code&gt; failed with "command not found" — indistinguishable, in code, from "partition is genuinely blocking traffic." &lt;code&gt;Verify&lt;/code&gt; reported every partition as confirmed even &lt;em&gt;after&lt;/em&gt; it had been cleared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NATS JetStream KV defaults to non-linearizable reads.&lt;/strong&gt; &lt;code&gt;CreateKeyValue&lt;/code&gt; unconditionally sets &lt;code&gt;AllowDirect: true&lt;/code&gt;, letting any replica (not just the Raft leader) answer a read — a documented, deliberate latency/consistency tradeoff. Left on, an early batch produced 3 false violations with single-operation counterexamples impossible from an empty initial state: a stale replica read, not a real bug. Fixed by disabling it once, forcing every read through the leader.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixing #5 the obvious way created a new bug.&lt;/strong&gt; Doing that disable from every workload client's own &lt;code&gt;Connect&lt;/code&gt; call created a thundering herd of concurrent reconfiguration attempts on a just-booted cluster — slow enough to time out &lt;code&gt;Connect&lt;/code&gt; itself. Fixed by moving it into a one-time &lt;code&gt;Bootstrap&lt;/code&gt; step.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Findings: etcd, 100 runs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Valid, distinct successful seeds&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linearizability violations&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inconclusive checks&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invalid attempts&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total recorded operations&lt;/td&gt;
&lt;td&gt;35,470&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Successful operations&lt;/td&gt;
&lt;td&gt;35,378&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ambiguous operations&lt;/td&gt;
&lt;td&gt;92&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confirmed operation failures&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Successful recovery operations&lt;/td&gt;
&lt;td&gt;1,500&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fault&lt;/th&gt;
&lt;th&gt;Overlapping ops&lt;/th&gt;
&lt;th&gt;Successful&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;delay&lt;/td&gt;
&lt;td&gt;443&lt;/td&gt;
&lt;td&gt;443&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;drop&lt;/td&gt;
&lt;td&gt;707&lt;/td&gt;
&lt;td&gt;705&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kill&lt;/td&gt;
&lt;td&gt;405&lt;/td&gt;
&lt;td&gt;380&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;partition&lt;/td&gt;
&lt;td&gt;463&lt;/td&gt;
&lt;td&gt;446&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reorder&lt;/td&gt;
&lt;td&gt;486&lt;/td&gt;
&lt;td&gt;486&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both invalid attempts are kept in the dataset, not discarded — one (seed 1006) was later retried independently and succeeded; both artifacts remain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result: zero violations across 35,378 successful operations and 100 valid runs covering all five standard fault types.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings: NATS JetStream, a second real target
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Seed&lt;/th&gt;
&lt;th&gt;Validity&lt;/th&gt;
&lt;th&gt;Ops&lt;/th&gt;
&lt;th&gt;Ambiguous&lt;/th&gt;
&lt;th&gt;Recovery&lt;/th&gt;
&lt;th&gt;Verified faults&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;td&gt;713&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;20/20&lt;/td&gt;
&lt;td&gt;reorder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;301&lt;/td&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;td&gt;832&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;20/20&lt;/td&gt;
&lt;td&gt;partition, reorder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;302&lt;/td&gt;
&lt;td&gt;invalid&lt;/td&gt;
&lt;td&gt;834&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;19/20&lt;/td&gt;
&lt;td&gt;delay, kill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;303&lt;/td&gt;
&lt;td&gt;invalid&lt;/td&gt;
&lt;td&gt;849&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;19/20&lt;/td&gt;
&lt;td&gt;drop, kill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;304&lt;/td&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;td&gt;1093&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;20/20&lt;/td&gt;
&lt;td&gt;partition, reorder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;305&lt;/td&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;td&gt;913&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;20/20&lt;/td&gt;
&lt;td&gt;partition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;306&lt;/td&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;td&gt;495&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;20/20&lt;/td&gt;
&lt;td&gt;kill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;307&lt;/td&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;td&gt;1012&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;20/20&lt;/td&gt;
&lt;td&gt;delay, drop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;308&lt;/td&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;td&gt;1148&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;20/20&lt;/td&gt;
&lt;td&gt;delay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;309&lt;/td&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;td&gt;1258&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;20/20&lt;/td&gt;
&lt;td&gt;partition&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;8/10 valid, 0 violations, all 5 fault types verified. The two invalid runs have an understood cause: with direct-get disabled, a client routed through a partition-isolated node waits on an internal leader-forward that can outlast the fault's own duration — expected CP-system behavior, not a bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clock skew: the case for not taking the obvious shortcut
&lt;/h2&gt;

&lt;p&gt;Docker containers share their host's wall clock. &lt;code&gt;date -s&lt;/code&gt; inside a container — the naive approach — changes the clock for &lt;em&gt;every&lt;/em&gt; container on that daemon, not just the target; on Docker Desktop that's the whole VM. So ordinary Docker mode simply rejects &lt;code&gt;clock_skew&lt;/code&gt; by design.&lt;/p&gt;

&lt;p&gt;The real fix instruments one Go process directly: an optional Linux/amd64 &lt;code&gt;ptrace&lt;/code&gt;-based launcher clears the vDSO discovery flag in the traced child, then adjusts only successful &lt;code&gt;CLOCK_REALTIME&lt;/code&gt;/&lt;code&gt;gettimeofday&lt;/code&gt; reads — monotonic time, and therefore Go's own timers, stay untouched. &lt;code&gt;Verify&lt;/code&gt; requires a &lt;em&gt;fresh&lt;/em&gt; intercepted sample matching the request; a stored config value can't pass. Both a +2s and a −2s step against a real etcd process produced linearizable histories with successful recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Methodology
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verify, don't assume&lt;/strong&gt; — every fault is independently confirmed in effect.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Invalid attempts are retained, never silently retried away.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exhausted search is inconclusive, not a pass.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The checker is validated before being trusted&lt;/strong&gt; on any real target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every artifact is self-describing and replayable&lt;/strong&gt; — seed, config, actual fault timing, image identity, source/binary SHA-256 hashes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Scope
&lt;/h2&gt;

&lt;p&gt;This project does not claim Byzantine fault tolerance testing, simultaneous fault combinations (faults are currently serialized), leader-directed fault selection, or portability beyond the tested Docker Desktop/Linux environment. No violation found is reported as a bounded, honest negative result — not proof of absence of bugs under untested conditions.&lt;/p&gt;




&lt;p&gt;Source, every recorded artifact, and CI configuration: &lt;strong&gt;github.com/patelpratyush/faultline&lt;/strong&gt; (MIT licensed).&lt;/p&gt;

&lt;p&gt;Reproduce any run offline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go run ./cmd/faultline check &lt;span class="nt"&gt;-artifact&lt;/span&gt; results/final-b/seed-1101.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>go</category>
      <category>distributedsystems</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
