Skip to content

Commit 3c8b2b0

Browse files
npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67dTyler Longwell
andcommitted
Gate replica cursor routing behind a proven freshness fence
Cursor pages routed to the read replica were only safe if every row the page could contain had already replayed there. Commit order is not created_at order: a transaction carrying an older client-signed created_at can commit late, and a replica page paginating past that gap skips the row permanently (the keyset cursor never revisits it). Close that hole with a two-part proof: 1. Commit-time floor (migration 0021): a DEFERRABLE INITIALLY DEFERRED constraint trigger on events re-checks, inside COMMIT processing and against clock_timestamp() (not the transaction-frozen now()), that channel-bearing rows are no older than buzz.created_at_floor seconds. The relay's writer pool arms the GUC on every connection (after_connect), turning the ingest-time +/-900s envelope into a storage invariant that every insert path inherits — including the writers that bypass ingest_event entirely (workflow sink, side effects, moderation notices, audio, git transport, replace_*). channel_id-NULL rows (push leases, discovery snapshots) are structurally exempt; they never appear in keyset windows. Startup fails closed if any events partition lacks the trigger. 2. Ordered LSN handshake (replica_fence): on one pinned writer connection, three separately-awaited statements sample S = clock_timestamp(), then scan pg_stat_activity (plus pg_prepared_xacts) for the oldest open transaction, then capture L = pg_current_wal_lsn() last. Once the replica reports pg_last_wal_replay_lsn() >= L, every transaction falls into exactly one of three buckets — replayed, bounded by oldest_xact_start, or floor-guarded after S — so the fence advances to min(oldest_xact_start, S) - floor - margin with no timeout assumptions at all. Everything fails closed: probe errors, masked pg_stat_activity rows (an unprivileged view masks backend_type itself — detected before any backend-type filter), NULL replay LSN, a not-in-recovery 'replica', non-advancing replay, and probe staleness all close the fence and route every cursor page back to the writer. Routing now consults the fence: channel windows serve a cursor from the replica only when the fence covers the cursor timestamp; thread pages only when the page is full AND its tail is at or below the fence (under-limit terminal pages still re-verify on the writer). Adversarial fixtures pin both inversions (DESC middle hole, ASC full-page skip), the held-transaction commit abort, the armed-pool end-to-end rejection with SHOW verification, the masked-activity fail-closed path, and the NULL-replay-LSN breaker. Co-authored-by: Tyler Longwell <[email protected]> Signed-off-by: Tyler Longwell <[email protected]>
1 parent 50ae82a commit 3c8b2b0

6 files changed

Lines changed: 1108 additions & 27 deletions

File tree

0 commit comments

Comments
 (0)