Test physical replication and standby redo (#241) - #251
Conversation
recovery.sh covers crash redo in place. Nothing stood up a replica, and wal_envelope.sh only greps the C source for the envelope around the one direct XLogInsert, so the record had never been executed, let alone replayed. test/replication.sh does pg_basebackup to a second cluster, streams a columnar workload, and asserts the standby matches the primary after replay: insert, delete, update, vacuum, compact, and the SMGR truncate record. ## A correction to the issue's premise #241 says standby replay "requires the module loaded on the standby with the correct redo routines". That is not so. Every record this extension emits is a CORE type: data pages via log_newpage / log_newpage_buffer (RM_XLOG_ID), and one XLogInsert(RM_SMGR_ID, XLOG_SMGR_TRUNCATE). There is no custom resource manager, so redo is core's. The module is needed to READ a columnar table, because that needs the access method handler, which is a different thing from replaying the bytes. Scenario 3 asserts exactly that: a standby with shared_preload_libraries emptied still starts, replays columnar WAL past the primary's LSN, does not PANIC, and after the module is put back reads the table it replayed while blind to it. That turns the standing WAL constraint from an argument into a test. ## The controls, which are the point A replication suite that never replicates passes every content comparison. So: the standby is asserted to be in recovery and to be a different data directory, and there is a positive control that pauses replay, writes on the primary, and requires the standby NOT to see it before resuming and requiring that it does. Proven by removal: dropping -R from pg_basebackup, so the copy is not a replica at all, fails 13 of 21 checks including the in-recovery control, every content hash and the truncate assertions. ## Two things that cost time and are recorded in the file The SMGR truncate record is hard to emit and easy to think you have emitted. End truncation is opt-in (GUC default off), and truncBlock comes from the highest LIVE row group, so a DELETE alone reclaims nothing. My first attempt used default limits and 60,000 rows: truncate returned 0 every time and pg_waldump confirmed zero SMGR TRUNCATE records in the whole run, while three assertions about replaying that record passed. The suite now asserts the primary actually truncated before claiming anything about the standby, and that precondition failed loudly until the fixture was right. The fixture is copied from native_truncate.sh rather than invented, and each statement is its own psql call: psql -c with several statements wraps them in one implicit transaction, and set_options and compact must be committed before the next step reads their effect. Batching them was why the first fixture silently reclaimed nothing. Verified 21/21 on PG17.10. Registered in run_all_versions.sh; harness_selftest green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@ChronicallyJD ready for review. Full five-major matrix is running on this branch The part most worth your scepticism is Scenario 3, because it is the one that Second thing worth checking: I assert the primary actually truncated before |
|
Attacked it where you asked — Scenario 3 and the truncate precondition — and both hold. The one thing I'd block on is the standby port: it's the collision you suspected, confirmed with the exact mechanism. Scenario 3 (module-less redo) — sound, and it settles the argumentThe standby restarts with Truncate precondition — real, hole closed
Vacuity controls — sound, couldn't break them
Block on this: the standby port collides by construction in the matrix
Fix: pick Two minor
Fix the port and this is a strong suite — it's the first thing that actually executes and replays the WAL envelope instead of grepping for it. |
The full five-major matrix failed this suite on PG18, then on PG19 after the
first fix, while it passed standalone on both. Two distinct causes, both mine.
The standby port was PGC_PORT + 1, which is the one value that cannot work.
run_all_versions.sh gives each suite a port by walking upward from a base, one
per suite per major, so PGC_PORT + 1 is the next suite's primary by
construction. portlib.sh documents that walk; I wrote the bug and read the
comment afterwards. The standby port now comes from below the matrix's range and
is verified free rather than assumed free.
That exposed the real one. pg_ctl -w gives up after 60 seconds and its exit
status was being discarded, so under the full matrix -- six suites and their
clusters on one box -- the standby sometimes had not reached a consistent state
in time. The suite carried on regardless and reported twelve empty-result
failures downstream, none of which said "the standby never started". sb_start
now waits for the standby to actually answer and, when it does not, prints the
standby's own log instead of letting the cascade speak for it.
The captured failure was unambiguous once the log survived teardown:
-- primary port 49080, standby port 34152
PASS pg_basebackup produced a data directory
FAIL the standby accepts connections: got [] want [1]
... eleven more empty-result failures
Verified: full five-major matrix ALL VERSIONS PASSED, 490 suite runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The matrix caught the thing I flagged, and then a second thing behind it. Both 1. The port, which I predicted and still got wrong
2. The one actually worth having foundFixing the port moved the failure from PG18 to PG19, which is the signature of a
How it was foundNeither reproduced under four concurrent copies of this suite, nor under a |
|
Fix confirmed against the diff:
The two minors I raised are now low priority precisely because the port fix removed the condition that made them matter: the The race is the good find -- start-wait races that only appear under the full five-major run, snapshotted from a per-major builddir that's deleted after, are exactly the kind that eat a day. Recording the snapshot trick in the file is the right call. |
@ChronicallyJD
recovery.shcovers crash redo in place. Nothing has ever stoodup a replica, and
wal_envelope.shonly greps the C source for the envelopearound the one direct
XLogInsert— so that record had never been executed,let alone replayed.
test/replication.sh:pg_basebackupto a second cluster, stream a columnarworkload, assert the standby matches after replay. 21 checks, ~40s.
A correction to the issue's premise
#241 says standby replay "requires the module loaded on the standby with the
correct redo routines". It does not. Every record emitted is a core type —
data pages through
log_newpage/log_newpage_buffer(RM_XLOG_ID), and oneXLogInsert(RM_SMGR_ID, XLOG_SMGR_TRUNCATE). There is no custom rmgr, so redo iscore's. The module is needed to read a columnar table, which needs the access
method handler, and that is a different thing from replaying the bytes.
Scenario 3 asserts it: a standby with
shared_preload_librariesemptied starts,replays columnar WAL past the primary's LSN, does not PANIC, and once the module
is restored reads the table it replayed while blind to it. That turns the
standing WAL constraint into a test instead of an argument, and it is the check I
would keep if I could keep only one.
The controls are the point
A replication suite that never replicates passes every content comparison. So the
standby is asserted to be in recovery and to be a different data
directory, and there is a positive control that pauses replay, writes on the
primary, requires the standby not to see it, then resumes and requires that it
does.
Proven by removal. Drop
-Rfrompg_basebackupso the copy is not areplica: 13 of 21 checks fail, including the in-recovery control, every
content hash, and the truncate assertions.
Two things that cost me time, both recorded in the file
The SMGR truncate record is hard to emit and easy to think you have emitted.
End truncation is opt-in (GUC default off), and
truncBlockcomes from thehighest live row group, so a
DELETEalone reclaims nothing. My firstattempt used default limits and 60,000 rows:
pgcolumnar.truncate()returned 0every time,
pg_waldumpconfirmed zero SMGR TRUNCATE records in the entirerun — and three assertions about replaying that record passed anyway. The suite
now asserts the primary actually truncated before claiming anything about the
standby, and that precondition failed loudly until the fixture was right.
I also mis-read
pgcolumnar.truncate()as SQLTRUNCATEat first. It isbest-effort physical end-truncation; rows are not meant to disappear. I nearly
filed that as a replication bug.
Each statement is its own
psqlcall, deliberately.psql -cwith severalstatements wraps them in one implicit transaction, and
set_optionsandcompactmust commit before the next step reads their effect. Batching them waswhy the first fixture silently reclaimed nothing.
Gate
21/21 on PG17.10, registered in
run_all_versions.sh,harness_selftestgreen.I have not run the full five-major matrix on it yet — worth doing before merge,
since it adds a suite that starts two clusters and the port arithmetic
(
SB_PORT = PGC_PORT + 1) is the kind of thing that only collides under thematrix's parallelism. Flagging it rather than discovering it there.