<?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: sqlrush</title>
    <description>The latest articles on DEV Community by sqlrush (@sqlrush).</description>
    <link>https://dev.to/sqlrush</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%2F3986376%2F3bda6ee8-0fcc-477e-83ae-36cee97a8de3.jpeg</url>
      <title>DEV Community: sqlrush</title>
      <link>https://dev.to/sqlrush</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sqlrush"/>
    <language>en</language>
    <item>
      <title>Fencing a node that doesn't know it's dead: pgrac build log #2</title>
      <dc:creator>sqlrush</dc:creator>
      <pubDate>Thu, 18 Jun 2026 12:55:13 +0000</pubDate>
      <link>https://dev.to/sqlrush/fencing-a-node-that-doesnt-know-its-dead-pgrac-build-log-2-4i81</link>
      <guid>https://dev.to/sqlrush/fencing-a-node-that-doesnt-know-its-dead-pgrac-build-log-2-4i81</guid>
      <description>&lt;p&gt;pgrac is an open attempt to rebuild Oracle RAC's core machinery (shared-everything storage, multiple active nodes all writing one database, a cluster-wide change number) on top of PostgreSQL 16. Build log #1 laid out the four problems that fight back. This one is about the problem that turns a node failure into silent data corruption, and the first, deliberately modest, layer pgrac ships against it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The failure mode
&lt;/h3&gt;

&lt;p&gt;In a shared-nothing cluster an evicted node is mostly harmless: it owns its own disks, so the cluster routes around it. In a shared-everything cluster the same event is dangerous, because every node writes the same storage. Picture the classic split: node 2 misses heartbeats, the cluster declares it dead and remasters its work elsewhere, but node 2 is not actually dead. It is frozen on a long GC pause, or its interconnect NIC flaked, and it is about to wake up and finish the write it started. Now two nodes believe they own the same blocks, and shared storage will accept both writes. That is not a crash. It is corruption you find three days later.&lt;/p&gt;

&lt;p&gt;Oracle RAC's answer is I/O fencing: before remastering a dead node's resources, you make certain it can no longer touch the storage, with STONITH, SCSI-3 persistent reservations, or a hardware watchdog. The node is fenced at a layer below its own software, because the whole point is that you cannot trust the dead node's software to behave.&lt;/p&gt;

&lt;p&gt;That hardware layer is real work, and it is not what pgrac built first. What it built first is the layer above it: an in-process cooperative write-fence, now default-ON. The rest of this is precise about what that does and does not buy you, because "we have fencing" is the kind of claim that is worth less than nothing if it is overstated.&lt;/p&gt;

&lt;h3&gt;
  
  
  A fence needs an authority everyone can agree on
&lt;/h3&gt;

&lt;p&gt;You cannot fence on local opinion, because the whole problem is that the dead node disagrees about being dead. Authority has to live on durable, shared, quorum-backed storage.&lt;/p&gt;

&lt;p&gt;pgrac writes a small fence marker into the cluster's voting disks, the same disks used for membership. The marker is a compact tuple: a monotonic &lt;code&gt;fence_epoch&lt;/code&gt;, a &lt;code&gt;fence_generation&lt;/code&gt; tie-break, a &lt;code&gt;fenced_dead_bitmap&lt;/code&gt; naming which nodes are declared dead, and the issuer's id, CRC-protected inside the voting slot. Reconfiguration bumps the epoch and submits the new marker. It is written to a quorum-majority of voting disks, and the reconfiguration proceeds only if a majority acknowledges. If the majority write fails, it fails closed: no event is published and no remastering starts. A minority of disks cannot amplify itself into an authority, because each disk preserves only its own marker, so a single stale disk cannot out-vote the quorum.&lt;/p&gt;

&lt;p&gt;Every node runs a poller (&lt;code&gt;qvotec&lt;/code&gt;) that reads all voting disks every couple of seconds, keeps only CRC-valid markers, orders them by &lt;code&gt;(fence_epoch DESC, fence_generation DESC)&lt;/code&gt;, and recognizes an authority only when it sees the same marker on a majority of disks. That majority requirement is the split-brain guard. It is what stops two partitions from each minting their own truth.&lt;/p&gt;

&lt;h3&gt;
  
  
  The hot path: check a token before touching storage
&lt;/h3&gt;

&lt;p&gt;Consulting voting disks on every write would be absurd, so the poller distills the durable marker into a small local token in shared memory: the epoch this node is authorized for, a lease expiry, a &lt;code&gt;self_fenced&lt;/code&gt; bit, and a copy of the dead bitmap. The write path consults only that token, through a pure, lock-free, allocation-free judge that is safe to call inside a critical section. A write is allowed only when:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;enforcement_on&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;region_attached&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;epoch&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;authorized_epoch&lt;/span&gt;     &lt;span class="c1"&gt;// exact ==, not &amp;gt;=&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;lease_expire&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;self_fenced&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;==&lt;/code&gt; rather than &lt;code&gt;&amp;gt;=&lt;/code&gt; is deliberate: a node holding a stale epoch is as dangerous as one explicitly fenced, so anything other than the exact current epoch is refused. Six storage entry points (create, unlink, extend, zero-extend, write, truncate) call this judge before writing shared storage. A fenced or stale node fails closed: &lt;code&gt;ERROR 53R51&lt;/code&gt; on the normal path, &lt;code&gt;PANIC&lt;/code&gt; if it is caught in a critical section where it cannot safely back out. The lease makes silence safe: if a node is cut off from the voting disks and cannot refresh its token, the lease expires and the node fences itself. The safe default, when you have lost contact with the authority, is to stop writing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why default-ON was hard
&lt;/h3&gt;

&lt;p&gt;The hard part was not fencing a dead node. It was not fencing a healthy one. Fail-closed-on-silence is correct, but it has a corollary: a healthy cluster, idle with nobody fenced, has no fence marker on the voting disks. No marker means the poller never refreshes the token, the lease expires, and every node fences itself, so the whole healthy cluster cannot write. The safety mechanism starves the happy path. That is why enforcement could not just be flipped on in the first cut.&lt;/p&gt;

&lt;p&gt;The fix is a steady-state baseline marker: even with nobody fenced, the cluster continuously maintains a current, majority-agreed "all alive at epoch N" marker, so the token lease always renews and a healthy cluster writes normally, while a real fence still supersedes it the instant it is issued. Getting the priority right between baseline and real-fence markers is itself a split-brain hazard, since a stale baseline author must never overwrite a higher-epoch fence. That is what two hardening passes after the initial flip were about: a baseline author may not regress the epoch, and a node must engage its bring-up latch before it ever publishes a self-fencing token. Only after that does &lt;code&gt;cluster.write_fence_enforcement&lt;/code&gt; default to on for a multi-node shared-FS cluster. A single-node instance, or one with no voting disks, auto-degrades to a no-op and opens cleanly. You do not pay for cluster fencing when there is no cluster.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is demonstrated, and what is not
&lt;/h3&gt;

&lt;p&gt;Demonstrated, in CI, on shared-FS clusters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On a 2-node and a 3-node cluster with enforcement default-ON, when a node is declared dead and self-fences, all six storage entry points reject the write with &lt;code&gt;53R51&lt;/code&gt;, verified end-to-end (&lt;code&gt;t/271&lt;/code&gt;, &lt;code&gt;t/272&lt;/code&gt;), with a pre-injection baseline write proving the test is not falsely green.&lt;/li&gt;
&lt;li&gt;The reconfiguration, marker-write, and fence path is exercised by a synthetic dead-node injection that proves the mechanism runs to completion. A counter delta proves the code path executed, rather than being dead code. This is the recovery acceptance mechanism-completion gate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deliberately not claimed yet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is cooperative, not hardware, fencing. It fences a node that is still running the gate. It does nothing about a node whose software is wedged in a way that bypasses the gate. That is what STONITH, SCSI-3 PR, and cloud soft-fence are for, and that external fence plane is future work, not in this code.&lt;/li&gt;
&lt;li&gt;Faithful-crash auto-recovery is not proven in CI. The synthetic injection proves the mechanism; a real &lt;code&gt;SIGKILL&lt;/code&gt; of a node followed by deterministic apply-through recovery is SKIP-with-limitation in the single-machine test harness, and labeled as such. That is not true N-node auto-recovery until it runs against a real multi-machine backend.&lt;/li&gt;
&lt;li&gt;No true 4-node CI demo. The 4-node recovery demo is a manual script that takes an external cluster connection string; with no such backend wired up it SKIPs with a reason rather than faking a pass.&lt;/li&gt;
&lt;li&gt;Being fenced is terminal in this stage. A fenced node comes back up in a non-serving mode: it refuses shared writes and waits for offline or cold-admin recovery. Online rejoin, a fenced node automatically returning to active service, is Stage 5 and later.&lt;/li&gt;
&lt;li&gt;Steady 2-node OLTP throughput and cross-node Cache Fusion block transfer remain Stage 5 problems. Nothing here changes that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest one-liner: pgrac now has a default-on, quorum-backed, fail-closed cooperative write-fence, demonstrated to stop a self-fenced node from writing shared storage on 2- and 3-node clusters. It is the first of several fencing layers, with the hardware layer and faithful-crash recovery still ahead. That is a real step, and it is one layer of a problem that has several. I would rather undersell it than have a RAC veteran catch me overselling.&lt;/p&gt;

&lt;p&gt;Code is in the open: &lt;code&gt;cluster_write_fence.{c,h}&lt;/code&gt;, the storage gates in &lt;code&gt;cluster_smgr.c&lt;/code&gt;, the durable marker and poller in &lt;code&gt;cluster_qvotec.c&lt;/code&gt;, and the tests under &lt;code&gt;src/test&lt;/code&gt; (&lt;code&gt;t/271&lt;/code&gt;, &lt;code&gt;t/272&lt;/code&gt;). &lt;a href="https://github.com/sqlrush/pgrac" rel="noopener noreferrer"&gt;https://github.com/sqlrush/pgrac&lt;/a&gt; · status board: &lt;a href="https://pgrac.dev/features" rel="noopener noreferrer"&gt;https://pgrac.dev/features&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have built I/O fencing for a shared-storage system and think the cooperative-first ordering is wrong, I want to hear it.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>distributedsystems</category>
      <category>oracle</category>
    </item>
    <item>
      <title>Rebuilding Oracle RAC's core machinery on PostgreSQL — the four problems that fight back</title>
      <dc:creator>sqlrush</dc:creator>
      <pubDate>Tue, 16 Jun 2026 00:41:34 +0000</pubDate>
      <link>https://dev.to/sqlrush/rebuilding-oracle-racs-core-machinery-on-postgresql-the-four-problems-that-fight-back-2dhl</link>
      <guid>https://dev.to/sqlrush/rebuilding-oracle-racs-core-machinery-on-postgresql-the-four-problems-that-fight-back-2dhl</guid>
      <description>&lt;p&gt;&lt;strong&gt;pgrac&lt;/strong&gt; is an attempt to build many of Oracle RAC's core capabilities — shared-everything storage, multiple &lt;em&gt;active&lt;/em&gt; nodes all writing one database, Cache Fusion, a cluster-wide change number — directly on PostgreSQL 16, in the open.&lt;/p&gt;

&lt;p&gt;Be precise about the gap it aims at. As far as I can tell there is no other open-source, multi-active, Cache-Fusion cluster on Postgres. PolarDB and Aurora are shared-storage, but &lt;strong&gt;single-writer&lt;/strong&gt; — one node writes, the rest read. BDR, Citus, Patroni and friends are &lt;strong&gt;shared-nothing&lt;/strong&gt;. If you know of a real open-source, every-node-writes Postgres on shared storage, I genuinely want to see it.&lt;/p&gt;

&lt;p&gt;This isn't a "look what I shipped" post. Most of this is hard and in progress. It's a "here is what the machinery actually is, and why each piece fights you" post. Four problems sit at the core. None is optional, and each one fails &lt;em&gt;silently&lt;/em&gt; if you get it wrong — which is the worst way for a database to fail.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2tl8e4vu2bx35z1i6d81.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2tl8e4vu2bx35z1i6d81.png" alt="pgrac cluster topology" width="799" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1 — A clock every node can agree on
&lt;/h2&gt;

&lt;p&gt;Single-node Postgres orders changes with two things: transaction ids and the WAL LSN. In a shared-everything cluster, both stop working as a global order.&lt;/p&gt;

&lt;p&gt;A 32-bit &lt;code&gt;TransactionId&lt;/code&gt; is only meaningful &lt;em&gt;inside one instance&lt;/em&gt; — node A's xid 5000 and node B's xid 5000 are different transactions, in the same numeric space. And LSNs from two independent WAL streams are simply incomparable; pgrac's own recovery code says so in as many words. So when a reader on node A meets a row last written by node B, neither xid nor LSN tells it "did B's write happen before my snapshot?"&lt;/p&gt;

&lt;p&gt;RAC's answer is the &lt;strong&gt;SCN&lt;/strong&gt; — a System Change Number, a single monotonic clock the whole cluster shares. The naive way to build one is a central allocator: one node (or service) hands out the next number. That is fatal on the hot path. Every commit would need a synchronous round-trip to the allocator &lt;em&gt;before it could become durable&lt;/em&gt;, turning a node-local WAL insert into a network-bound operation, and making the allocator both a throughput ceiling and a single point of failure.&lt;/p&gt;

&lt;p&gt;So pgrac builds the SCN as a &lt;strong&gt;distributed Lamport clock&lt;/strong&gt;. Two operations, both lock-free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;advance&lt;/strong&gt; (on commit) is a single atomic &lt;code&gt;fetch_add&lt;/code&gt; on a per-node counter — about 5–50 ns, contends on nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;observe&lt;/strong&gt; (on receiving anything from another node) is &lt;code&gt;current = max(current, remote) + 1&lt;/code&gt;, done as a compare-and-swap retry loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The clock then synchronizes for free: every message on the interconnect — heartbeat, lock grant, block transfer — carries the sender's current SCN in its envelope header, and the receiver bumps its own clock before the message is even dispatched. No dedicated clock protocol exists; it's Lamport's "piggyback timestamps on all messages," literally.&lt;/p&gt;

&lt;p&gt;The subtle part is comparison. An SCN packs &lt;code&gt;[8-bit node_id | 56-bit local counter]&lt;/code&gt; into a &lt;code&gt;uint64&lt;/code&gt;. If you compare two SCNs with a raw &lt;code&gt;&amp;lt;&lt;/code&gt;, the node_id in the high bits dominates and the ordering is garbage. Visibility comparisons must look at &lt;strong&gt;only&lt;/strong&gt; the local-counter bits and ignore node_id entirely — while a &lt;em&gt;different&lt;/em&gt; comparison, the one used to give ITL slots a globally-unique order, keeps node_id as a tiebreak. Getting this backwards is a silent-wrong-answer bug, so there's literally a CI check that greps for raw &lt;code&gt;&amp;lt;&lt;/code&gt;/&lt;code&gt;==&lt;/code&gt;/&lt;code&gt;&amp;gt;&lt;/code&gt; on SCN values and fails the build.&lt;/p&gt;

&lt;p&gt;Finally the SCN has to live in durable structures, not just memory: an 8-byte &lt;code&gt;pd_block_scn&lt;/code&gt; went into the page header (page layout version bumped 4 → 5), and an 8-byte &lt;code&gt;xl_scn&lt;/code&gt; into every WAL record. Recovery replays in SCN order; consistent reads compare against it. The clock is the foundation the other three problems stand on.&lt;/p&gt;

&lt;p&gt;→ &lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/sqlrush/pgrac/blob/main/src/backend/cluster/cluster_scn.c" rel="noopener noreferrer"&gt;&lt;code&gt;cluster_scn.c&lt;/code&gt;&lt;/a&gt; — &lt;code&gt;cluster_scn_advance&lt;/code&gt; is the fetch-add, &lt;code&gt;cluster_scn_observe&lt;/code&gt; the CAS-retry observe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2 — MVCC across nodes, without taxing the node that has no peers
&lt;/h2&gt;

&lt;p&gt;This is the one I find genuinely beautiful and genuinely scary.&lt;/p&gt;

&lt;p&gt;Postgres decides tuple visibility with &lt;code&gt;xmin&lt;/code&gt;/&lt;code&gt;xmax&lt;/code&gt; plus the commit log (CLOG). Here's the trap: a tuple on shared storage might have been written by &lt;em&gt;another instance&lt;/em&gt;. If node A resolves that tuple's &lt;code&gt;xmin&lt;/code&gt; through node A's &lt;strong&gt;local&lt;/strong&gt; CLOG, it will happily find &lt;em&gt;some&lt;/em&gt; transaction with that number — a completely unrelated local one — and return a confident, wrong answer. Silent cross-instance corruption. The 32-bit xid space overlapping across nodes means you can never, ever resolve a remote xid through local visibility machinery.&lt;/p&gt;

&lt;p&gt;But the obvious fix — route &lt;em&gt;every&lt;/em&gt; tuple through some cluster-wide visibility service — would tax the 99% case (a node doing ordinary local OLTP) to serve the 1% case (a tuple another node touched). That's unacceptable; nobody will run a cluster that halves their single-node throughput.&lt;/p&gt;

&lt;p&gt;pgrac's answer is &lt;strong&gt;dual-track visibility&lt;/strong&gt;, steered by a single byte. Each heap tuple header carries a new &lt;code&gt;t_itl_slot_idx&lt;/code&gt; (one byte; &lt;code&gt;255&lt;/code&gt; = unallocated). On every visibility check, a classifier reads the tuple's ITL (interested-transaction-list) slot and decides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;own-instance evidence&lt;/strong&gt; → fall straight through to native Postgres &lt;code&gt;xmin&lt;/code&gt;/&lt;code&gt;xmax&lt;/code&gt; + CLOG. Untouched. Zero cluster overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;remote evidence&lt;/strong&gt; → resolve through the cluster path: ITL slot → transaction-table → SCN → undo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;stale/ambiguous&lt;/strong&gt; (the slot was recycled to a different remote owner) → &lt;strong&gt;fail closed&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The payoff: a node with no live peers does one boolean comparison per tuple and runs vanilla Postgres. The whole cluster apparatus is bypassed unless a tuple is physically stamped by another node. You only pay for coherence on the rows that actually need it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkzcq2t0wir3h4ltm5vxp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkzcq2t0wir3h4ltm5vxp.png" alt="dual-track cluster MVCC" width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The remote path is where Oracle's ghosts show up. A cross-node tuple's visibility is decided by resolving its commit &lt;em&gt;status&lt;/em&gt; and commit &lt;em&gt;SCN&lt;/em&gt; through a transaction table, then comparing &lt;code&gt;commit_scn&lt;/code&gt; against the snapshot's &lt;code&gt;read_scn&lt;/code&gt;. If the current row is newer than your snapshot, pgrac reconstructs the older version Oracle-style: it walks the undo chain backward, newest-first, inverse-applying changes until it reaches a version at or before your &lt;code&gt;read_scn&lt;/code&gt;. That's Consistent Read, rebuilt on Postgres.&lt;/p&gt;

&lt;p&gt;Two design decisions are worth dwelling on, because they're what separate "demo" from "correct":&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visibility is three-valued, not two.&lt;/strong&gt; A cross-node resolution can come back VISIBLE, INVISIBLE, or &lt;em&gt;UNKNOWN&lt;/em&gt; — and UNKNOWN is never silently collapsed into INVISIBLE. If a remote commit SCN hasn't propagated yet, or an overlay entry is missing, the read raises a specific error (&lt;code&gt;53R97&lt;/code&gt;, "TT status unknown") and lets the caller retry or abort. A wrong-but-silent "not visible" is exactly the bug this whole layer exists to prevent, so the code refuses to guess.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ABA problem is real and handled.&lt;/strong&gt; The on-page ITL is only an 8-slot cache; a remote peer can reuse a slot for a later transaction — even a &lt;em&gt;wrapped&lt;/em&gt; xid that numerically equals an old one. Resolving that against the peer's durable transaction table uses a 16-bit &lt;code&gt;wrap&lt;/code&gt; generation counter, so a same-valued-but-wrapped xid is a sound zero-match (fail closed) instead of a dangerous false match. The durable outcome is only trusted when the SLRU commit state and the independent transaction-table slot agree on &lt;em&gt;both&lt;/em&gt; commit SCN &lt;em&gt;and&lt;/em&gt; wrap generation.&lt;/p&gt;

&lt;p&gt;Honest status: the dual-track resolver, the ITL write path, and decide-by-SCN are wired into every &lt;code&gt;HeapTupleSatisfies*&lt;/code&gt; function and exercised by 2-node tests. What's still maturing is &lt;em&gt;live&lt;/em&gt; cross-node undo reads (versus the crash-recovery materialized path) and breadth of multi-node behavioral coverage.&lt;/p&gt;

&lt;p&gt;→ &lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/sqlrush/pgrac/blob/main/src/backend/cluster/cluster_visibility_resolve.c" rel="noopener noreferrer"&gt;&lt;code&gt;cluster_visibility_resolve.c&lt;/code&gt;&lt;/a&gt; (the dual-track classifier) and &lt;a href="https://github.com/sqlrush/pgrac/blob/main/src/backend/cluster/cluster_cr.c" rel="noopener noreferrer"&gt;&lt;code&gt;cluster_cr.c&lt;/code&gt;&lt;/a&gt; (consistent-read construction from undo).&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3 — Moving a dirty page between two memories without losing it
&lt;/h2&gt;

&lt;p&gt;Cache Fusion is the headline RAC feature: when node B needs a block node A holds dirty, ship it straight from A's buffer cache into B's over the interconnect, instead of forcing A to write it to disk so B can read it back. That "disk ping" is two synchronous storage round-trips on the hot path and serializes every writer hand-off behind I/O — the exact thing Cache Fusion exists to kill.&lt;/p&gt;

&lt;p&gt;The protocol is three-way. The block has a &lt;strong&gt;master&lt;/strong&gt; — and the mastering is deterministic and coordinator-free: every node computes &lt;code&gt;master = hash(block_tag) mod (declared nodes)&lt;/code&gt; independently and gets the same answer, so there's no directory server to ask. A requester asks the master; the master forwards to the current holder; the holder ships the page directly back to the requester.&lt;/p&gt;

&lt;p&gt;The correctness-critical step is one line of intent: &lt;strong&gt;flush the WAL up to this page's LSN before the bytes leave the node.&lt;/strong&gt; Here's why it's mandatory and not just careful. pgrac ships the &lt;em&gt;dirty in-memory image&lt;/em&gt; — that page never went to shared storage. The receiver installs it and may build more redo on top. If the holder then crashes and the WAL describing how that page reached its current LSN was never durable, recovery cannot reconstruct the page the receiver already built on. That's a cross-node lost write, the kind that surfaces as corruption days later. Flushing WAL-up-to-&lt;code&gt;page_lsn&lt;/code&gt; before shipping makes the redo chain behind the image durable first.&lt;/p&gt;

&lt;p&gt;The other subtle bit is the ownership hand-off. The master flips "who owns this block exclusively" to the new writer &lt;strong&gt;only after&lt;/strong&gt; the new writer reports it has durably installed the image — not when the request is granted. That ordering closes the window where two nodes could both believe they hold the block exclusive and both write it.&lt;/p&gt;

&lt;p&gt;Honest status, and I want to be exact here because it's easy to overclaim: the data plane — memory-to-memory ship, WAL-flush-before-ship, CRC, LSN re-stamping, the starvation guard, the invalidate-and-ack broadcast — is all built and wired. First-touch acquisition and read-sharing run on the real lock path. But transferring a block &lt;em&gt;away from a still-live remote writer&lt;/em&gt; (the X-to-X case) is currently &lt;strong&gt;bounded fail-closed&lt;/strong&gt; — the master returns &lt;code&gt;FEATURE_NOT_SUPPORTED&lt;/code&gt; rather than do a transfer it can't yet prove correct under concurrent recovery. The hard case is implemented but gated off until the warm-recovery substrate underneath it lands. That's the honest line: the machinery is there; the last mile of live writer transfer is deliberately closed, not faked.&lt;/p&gt;

&lt;p&gt;→ &lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/sqlrush/pgrac/blob/main/src/backend/cluster/cluster_gcs_block.c" rel="noopener noreferrer"&gt;&lt;code&gt;cluster_gcs_block.c&lt;/code&gt;&lt;/a&gt; (the 3-way request / forward / invalidate handlers) and &lt;a href="https://github.com/sqlrush/pgrac/blob/main/src/backend/cluster/cluster_pcm_lock.c" rel="noopener noreferrer"&gt;&lt;code&gt;cluster_pcm_lock.c&lt;/code&gt;&lt;/a&gt; (the acquire path).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7rbkblcevslpzi8hynlq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7rbkblcevslpzi8hynlq.png" alt="Cache Fusion 3-way protocol" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 4 — Not corrupting everything when the network splits
&lt;/h2&gt;

&lt;p&gt;Multi-active is where split-brain stops being a slide and becomes the thing that eats your data. If the interconnect partitions and both sides keep accepting writes to shared storage, you get two divergent truths on one set of blocks. There is no clean merge from there.&lt;/p&gt;

&lt;p&gt;pgrac's primary defense is a &lt;strong&gt;fail-closed gate at the commit boundary&lt;/strong&gt;. Inside &lt;code&gt;CommitTransaction&lt;/code&gt;, &lt;em&gt;after&lt;/em&gt; the transaction has done its work but &lt;em&gt;before&lt;/em&gt; the WAL commit record is flushed, a writable transaction checks whether this node still holds quorum. If not, the transaction is aborted — error &lt;code&gt;53R40&lt;/code&gt;, quorum lost.&lt;/p&gt;

&lt;p&gt;The placement is the entire point. A commit becomes durable, visible, and unrecoverable only at the instant its WAL commit record hits disk. The gate sits one step upstream of that flush. So a node that loses quorum mid-transaction can never turn that work into a durable commit; uncertainty resolves to &lt;strong&gt;abort&lt;/strong&gt;, never to a divergent durable write. A partitioned minority that still &lt;em&gt;thinks&lt;/em&gt; it's primary simply cannot make its writes stick.&lt;/p&gt;

&lt;p&gt;Quorum itself is a disk-based vote with a lease: a majority of voting disks, and a lease that the quorum process must keep renewing. If that process hangs, the lease expires on its own and the gate fail-closes — no broadcast required, no liveness assumption. On top of that, a cooperative &lt;strong&gt;write-fence&lt;/strong&gt; (epoch + lease + a quorum-majority marker on the voting disks) rejects shared-storage writes from a node whose epoch is stale or whose lease has aged out, with a &lt;code&gt;PANIC&lt;/code&gt; if it's caught mid-critical-section where a half-write can't be rolled back.&lt;/p&gt;

&lt;p&gt;Now the honest part, because a split-brain section that oversells is worse than useless. pgrac's fencing is &lt;strong&gt;cooperative and internal&lt;/strong&gt; — there is no external STONITH, no IPMI, no SCSI-3 reservation, no "shoot the other node in the head" yet. The safety rests on each node fail-closing &lt;em&gt;itself&lt;/em&gt;. That covers a cooperating node that loses quorum; it does &lt;strong&gt;not&lt;/strong&gt; cover a node hung at the kernel level mid-write that ignores its own gates — the exact case STONITH exists for, and which is on the roadmap, not in the tree. And one more limitation I'll state plainly because it's load-bearing: today the quorum signal is driven by voting-disk I/O health, not by peer-liveness, so a clean network partition between two healthy nodes that can both still reach the disks does not, by itself, trip the fence. These are documented limits, not marketing softening. Getting split-brain &lt;em&gt;fully&lt;/em&gt; right is years of work; what's here is a sound cooperative core with the failure modes written down.&lt;/p&gt;

&lt;p&gt;→ &lt;strong&gt;Code:&lt;/strong&gt; the commit gate lives in &lt;a href="https://github.com/sqlrush/pgrac/blob/main/src/backend/access/transam/xact.c" rel="noopener noreferrer"&gt;&lt;code&gt;xact.c&lt;/code&gt;&lt;/a&gt; (&lt;code&gt;CommitTransaction&lt;/code&gt;); quorum + lease in &lt;a href="https://github.com/sqlrush/pgrac/blob/main/src/backend/cluster/cluster_qvotec.c" rel="noopener noreferrer"&gt;&lt;code&gt;cluster_qvotec.c&lt;/code&gt;&lt;/a&gt;; the cooperative write-fence in &lt;a href="https://github.com/sqlrush/pgrac/blob/main/src/backend/cluster/cluster_fence.c" rel="noopener noreferrer"&gt;&lt;code&gt;cluster_fence.c&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it actually stands
&lt;/h2&gt;

&lt;p&gt;Running today, on real code paths: the SCN clock, dual-track cross-node MVCC, Cache Fusion's data plane (first-touch + read-sharing), cluster catalog invalidation, the cluster-aware storage manager, and the substrate (interconnect, heartbeat, multi-node bootstrap). In progress: live-holder Cache Fusion transfer, full cross-node GES locking, crash/instance recovery, and external fencing. The sanity anchor underneath all of it: the &lt;code&gt;--disable-cluster&lt;/code&gt; build is binary-identical to upstream PostgreSQL 16.13 and passes the full 219-test regression suite — the non-cluster path is trustworthy, the cluster path is young. Per-feature status, honestly maintained, lives at &lt;a href="https://pgrac.dev/features/" rel="noopener noreferrer"&gt;pgrac.dev/features&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you've operated RAC or any shared-storage cluster and want to be useful: clone it, read the design, and tell me where it's wrong. "This breaks under X" is the most valuable thing you can send me right now, and there are scoped &lt;code&gt;good first issue&lt;/code&gt;s if you want to get your hands in.&lt;/p&gt;

&lt;p&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/sqlrush/pgrac" rel="noopener noreferrer"&gt;https://github.com/sqlrush/pgrac&lt;/a&gt;&lt;/strong&gt; · architecture and the full feature map: &lt;strong&gt;&lt;a href="https://pgrac.dev" rel="noopener noreferrer"&gt;https://pgrac.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next build-log goes one level deeper into the interconnect and how membership survives a node dropping mid-transaction.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>distributedsystems</category>
      <category>oracle</category>
    </item>
  </channel>
</rss>
