Goal
Empirically prove archive-fed logical decoding works end-to-end: a disposable standby that recovers only from archived WAL (no connection to the primary) keeps its logical slot alive across catalog-prune recovery conflicts — pausing recovery and then auto-resuming — while a consumer receives the complete change stream with no gaps.
This complements the in-tree TAP test src/test/recovery/t/054_recovery_pause_on_slot_conflict.pl. That TAP test uses the framework's own archiving and validates the mechanism inside core. This PoC is the real-world end-to-end validation that cannot live in core because it depends on external tools (WAL-G, object storage, pg_recvlogical + test_decoding/wal2json).
Environment
Setup
Workload (on primary)
Decode node
Triggering the pause (core mechanism)
The conflict-pause only fires if the slot's catalog_xmin is behind the prune horizon when the prune record is replayed. A fast consumer keeps catalog_xmin advanced → no conflict.
End marker
Success criteria
Lag measurement (no streaming → use LSN/time arithmetic)
- Replay time-lag:
now() - pg_last_xact_replay_timestamp()
- Replay LSN-lag:
pg_wal_lsn_diff(<latest archived LSN>, pg_last_wal_replay_lsn())
- Consumer / slot lag:
pg_wal_lsn_diff(pg_last_wal_replay_lsn(), confirmed_flush_lsn)
Caveats / notes
- DDL is NOT decoded (DML only). The decode node applies DDL physically, so its catalog stays correct for decoding subsequent DML.
- TRUNCATE IS decoded (since PG11) — optional coverage on a decoded table.
Deferred to v2 (advanced)
- Consumer back-pressure / disk-full stress (e.g. 20 GB table, 10 GB consumer disk): exercises the unbounded-pause boundary.
- Built-in safety valve:
max_slot_wal_keep_size still applies during the pause (the checkpointer runs), so past the limit the slot is invalidated out-of-band and replay proceeds.
- Without it, the pause holds until the standby's own disk fills. The operator escapes via
pg_wal_replay_resume (give up the slot) or by growing the disk.
Goal
Empirically prove archive-fed logical decoding works end-to-end: a disposable standby that recovers only from archived WAL (no connection to the primary) keeps its logical slot alive across catalog-prune recovery conflicts — pausing recovery and then auto-resuming — while a consumer receives the complete change stream with no gaps.
This complements the in-tree TAP test
src/test/recovery/t/054_recovery_pause_on_slot_conflict.pl. That TAP test uses the framework's own archiving and validates the mechanism inside core. This PoC is the real-world end-to-end validation that cannot live in core because it depends on external tools (WAL-G, object storage,pg_recvlogical+test_decoding/wal2json).src/test/recovery/t/054_recovery_pause_on_slot_conflict.plEnvironment
0f24332) + the 3-commit patch. The primary could be vanilla/unpatched (same major version), but use identical builds to remove variables.recovery_pause_on_logical_slot_conflict=on. The primary needs no patch, no slot, and no connection to the standby.Setup
wal_level=logical,archive_mode=on,archive_timeout=1s, autovacuum on.Workload (on primary)
pgbenchat ~10 TPS against the 4 standard pgbench tables (INSERT/UPDATE).ADD COLUMN/DROP COLUMNevery ~10s = catalog churn.pg_class,pg_attribute,pg_type,pg_statistic) soPRUNE_ON_ACCESSrecords are emitted — do not rely on autovacuum timing.Decode node
restore_command(archive-only;has_streaming=off; fully decoupled — no primary connection).pg_recvlogical+test_decoding(orwal2json) running on the standby; it persists its own flush LSN.Triggering the pause (core mechanism)
The conflict-pause only fires if the slot's
catalog_xminis behind the prune horizon when the prune record is replayed. A fast consumer keepscatalog_xminadvanced → no conflict.pg_get_wal_replay_pause_state()until it returns'paused'— not a fixed sleep.End marker
psqlcommand emits a sentinel:pg_logical_emit_message(true, 'test', 'END-<token>')OR a marker row carrying the commit LSN.Success criteria
lost.pg_replication_slots/pg_stat_replicationempty on the primary).pg_wal_replay_pause), compare its physical table state (ground truth) vs the consumer's applied state up to X; resume; repeat.Lag measurement (no streaming → use LSN/time arithmetic)
now() - pg_last_xact_replay_timestamp()pg_wal_lsn_diff(<latest archived LSN>, pg_last_wal_replay_lsn())pg_wal_lsn_diff(pg_last_wal_replay_lsn(), confirmed_flush_lsn)Caveats / notes
Deferred to v2 (advanced)
max_slot_wal_keep_sizestill applies during the pause (the checkpointer runs), so past the limit the slot is invalidated out-of-band and replay proceeds.pg_wal_replay_resume(give up the slot) or by growing the disk.