From 743a6d8e625d48f06fa8d990da789f1b04a4be92 Mon Sep 17 00:00:00 2001 From: Ayush Tiwari Date: Tue, 28 Apr 2026 16:37:15 +0530 Subject: [PATCH v1] Fix stale relation close in sequence synchronization copy_sequences() passes a Relation pointer to get_and_validate_seq_info() for each publisher result row and closes the relation after processing the row. If pg_get_sequence_data() returned NULL sequence data for a later row, get_and_validate_seq_info() could return COPYSEQ_SKIPPED before assigning a new relation to that output argument. The caller would then close the stale Relation pointer left from the previous row, tripping the relcache refcount assertion in assert-enabled builds. This is not limited to concurrent drops. pg_get_sequence_data() also returns NULL data when the publisher connection user cannot read the sequence. Clear the output Relation before validation and clear the local pointer after closing it. Add a subscription test that syncs one readable sequence followed by a published sequence for which the replication user lacks SELECT. --- .../replication/logical/sequencesync.c | 9 +++-- src/test/subscription/t/036_sequences.pl | 36 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/backend/replication/logical/sequencesync.c b/src/backend/replication/logical/sequencesync.c index ec7e76abf93..29264f8d01c 100644 --- a/src/backend/replication/logical/sequencesync.c +++ b/src/backend/replication/logical/sequencesync.c @@ -246,6 +246,8 @@ get_and_validate_seq_info(TupleTableSlot *slot, Relation *sequence_rel, Form_pg_sequence local_seq; LogicalRepSequenceInfo *seqinfo_local; + *sequence_rel = NULL; + *seqidx = DatumGetInt32(slot_getattr(slot, ++col, &isnull)); Assert(!isnull); @@ -254,8 +256,8 @@ get_and_validate_seq_info(TupleTableSlot *slot, Relation *sequence_rel, (LogicalRepSequenceInfo *) list_nth(seqinfos, *seqidx); /* - * last_value can be NULL if the sequence was dropped concurrently (see - * pg_get_sequence_data()). + * The sequence data can be NULL if it is not accessible on the publisher + * (see pg_get_sequence_data()). */ datum = slot_getattr(slot, ++col, &isnull); if (isnull) @@ -565,7 +567,10 @@ copy_sequences(WalReceiverConn *conn) } if (sequence_rel) + { table_close(sequence_rel, NoLock); + sequence_rel = NULL; + } } ExecDropSingleTupleTableSlot(slot); diff --git a/src/test/subscription/t/036_sequences.pl b/src/test/subscription/t/036_sequences.pl index 471780a3585..53233920edf 100644 --- a/src/test/subscription/t/036_sequences.pl +++ b/src/test/subscription/t/036_sequences.pl @@ -221,4 +221,40 @@ $node_subscriber->wait_for_log( qr/WARNING: ( [A-Z0-9]+:)? missing sequence on publisher \("public.regress_s4"\)/, $log_offset); +########## +# A NULL sequence data row from the publisher must not make the subscriber +# close the previously synchronized sequence relation again. +########## + +$node_publisher->safe_psql( + 'postgres', qq( + CREATE ROLE regress_seq_repl LOGIN REPLICATION; + CREATE SEQUENCE regress_no_select; + SELECT nextval('regress_no_select'); + GRANT USAGE ON SCHEMA public TO regress_seq_repl; + GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO regress_seq_repl; + REVOKE ALL ON SEQUENCE regress_no_select FROM PUBLIC; + REVOKE ALL ON SEQUENCE regress_no_select FROM regress_seq_repl; +)); + +$node_subscriber->safe_psql('postgres', + qq(CREATE SEQUENCE regress_no_select;)); + +my $publisher_limited_connstr = + $node_publisher->connstr . ' dbname=postgres user=regress_seq_repl'; +$log_offset = -s $node_subscriber->logfile; + +$node_subscriber->safe_psql( + 'postgres', + "CREATE SUBSCRIPTION regress_seq_sub_no_select CONNECTION '$publisher_limited_connstr' PUBLICATION regress_seq_pub WITH (disable_on_error = true)" +); + +$node_subscriber->wait_for_log( + qr/WARNING: ( [A-Z0-9]+:)? missing sequence on publisher \("public.regress_no_select"\)/, + $log_offset); + +$result = $node_subscriber->safe_psql('postgres', 'SELECT 1'); +is($result, '1', + 'subscriber remains running after publisher returns NULL sequence data'); + done_testing(); -- 2.34.1