fix: pg changes committed behind a synchronous standby - #2272
Conversation
This comment has been minimized.
This comment has been minimized.
b128689 to
0cd6a8a
Compare
This comment has been minimized.
This comment has been minimized.
78b74d3 to
ed1c2dc
Compare
This comment has been minimized.
This comment has been minimized.
de4d0fe to
f4d5567
Compare
CRAP Score Report |
c43dee0 to
687b2ca
Compare
fb7c9d0 to
e85dd62
Compare
| exit; | ||
| end if; | ||
|
|
||
| boundary := commit_lsns[i]; |
There was a problem hiding this comment.
If we are concerned about decoding twice, we can save the extra pg_logical_slot_get_changes below by buffering all visible changes in the for loop, return them, and do a pg_replication_slot_advance to the last visible txn's LSN
There was a problem hiding this comment.
Hey @kabochya let me measure, it might work.
/edit the gains are minimal and adds more complexity so I'm not sure it's worth it 🤔
There was a problem hiding this comment.
That's fine too; mostly just raising this approach for completeness.
There was a problem hiding this comment.
Oh yeah ideas are welcome, we should measure and test all 👍🏻
829c884 to
1b1959c
Compare
1b1959c to
b1d9e88
Compare
| -- A drop-in replacement for pg_logical_slot_get_changes, taking and forwarding the same | ||
| -- plugin options, that holds back a change whose transaction has not settled yet. It works | ||
| -- the boundary out for itself, so it takes no upto_lsn. | ||
| CREATE OR REPLACE FUNCTION realtime.settled_changes( |
There was a problem hiding this comment.
Note that I deliberately didn't add a guard to run this function only when sync standby is detected otherwise we'd create kind of a fork in one of the most used functions which would complicate debug, support, etc.
There was a problem hiding this comment.
Update: added a feature flag to preserve the original functions while we test the new approach.
| # Retried because a pooler can drop the connection mid-statement and OrioleDB can block on | ||
| # OTablesMetaTranche. The schema is dropped before it is recreated, so a lost attempt leaves the | ||
| # database with no realtime schema and every migration then fails with 3F000. | ||
| defp reset_realtime_schema!(settings) do |
There was a problem hiding this comment.
Unfortunately adding Multigres is creating some flaky tests and this change was an attempt to fix no 'realtime' schema errors. An attempt because I still see such errors but less than before so it's a win still. But it will need a bit more work to eliminate all.
This comment has been minimized.
This comment has been minimized.
8c2f0b2 to
a5e6d94
Compare
…nt-loss # Conflicts: # lib/realtime/tenants/migrations.ex # priv/repo/tenant_db_dump_15.sql # priv/repo/tenant_db_dump_17.sql
…nt-loss # Conflicts: # README.md
|
🎉 This PR is included in version 2.140.1 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
PG Changes can drop an RLS authorized INSERT whose COMMIT is waiting for a synchronous standby.
Reported by @kabochya (Multigres) in https://github.com/kabochya/realtime/tree/multigres-visibility-gate and built on top of his findings.
synchronous_standby_names defaults to empty in PG and that has been the config used since forever which doesn't add any wait time and thus no message loss. Any non-empty value makes COMMIT wait for an ack and messages can be lost in that window.
Multigres does it automatically and any Postgres server with non-empty value would face the same message loss problem.
Solutions considered
pg_logical_slot_get_changes. No fix. This is what runs without a synchronous standby.(1) Run as one query to avoid buffering which would use arrays to store data in-memory capped at 1GB, so a batch that decodes past that fails on every poll and the slot never advances.
Tests
apply_rls, vs mainpg_changes_delivery.exs, four writers, RLS policy that reads the row.delivered alone, an in-flight transaction is deferred, a denied change is consumed, 200
concurrent writes arrive on Multigres, and commit order matches main.
4286579968, which the
xip/xmaxcheck reads as settled.upto_nchangescounts them. Atmax_changes100 a poll covers about 50 transactions, and an empty poll idles 500 ms(
poll_interval_ms * 5), which caps a stream of unpublished writes near 100 txn/s.list_changes.exs, withapply_rls, which dominates.restart_lsncaught up per rep, median of 10 runs on 17.6 and five elsewhere.Trade-offs
max_changesgetstops after the record that reachesupto_lsn(logicalfuncs.c), and a C marker'slsnis the end of its commit record (logical.c), so the read includes that commit. The docs only say "commit prior to the specified LSN".lsnis the end of its commit record (logical.c), andpg_replication_slot_advanceconfirms exactly that LSN (logical.c, docs).getstops oncereturned_rowsreachesupto_nchanges(17.6, unchanged since 9.4), while the docs say it stops when the count exceeds it. Logical messages are found by wal2json v2's"action":"M"andtransactionalfields (wal2json 2.6).Proposal
Adopt peek+get (no markers, optimized) solution.
Introduce
realtime.list_changes_syncwhich is a drop-in replacement ofrealtime.list_changescalled only when it needs to await for standbys (when synchronous_standby_names is set). How it works:Call the function or not
No feature flag because this affects Multires and OrioleDB so I decided to probe the setting
synchronous_standby_namesand "fork" based on the DB:list_changeslist_changes_syncBench
dev/bench/list_changes.exslist_changeslist_changes_syncHigher delta with less changes is because any variation in timing cause more % with less messages as a consequence.
Delivery
dev/bench/pg_changes_delivery.exsRows lost, out of 500.
Note that on single-node Postgres without
synchronous_standby_namesthere's no loss because it doesn't need to wait for a standby.Notes
maindoesn't exercise that scenario.Refs
Closes REAL-1128