From efc021670d34d2b8bd87944202b9fb41f678d0ca Mon Sep 17 00:00:00 2001 From: Vignesh C Date: Tue, 28 Jul 2026 11:22:40 +0530 Subject: [PATCH v1 2/2] Add a TAP test to reproduce sequence sync lock table exhaustion Add a regression test that reproduces the failure where the sequence synchronization worker exhausts the subscriber's shared lock table while gathering sequences to synchronize. The test configures a small shared lock table and creates enough sequences in the INIT state so that the worker opens all of them with RowExclusiveLock in a single transaction, exhausting the available lock entries. It then verifies that the worker fails with the expected "out of shared memory" error and the corresponding hint to increase max_locks_per_transaction. --- src/test/subscription/meson.build | 1 + .../t/039_sequencesync_lock_exhaustion.pl | 120 ++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 src/test/subscription/t/039_sequencesync_lock_exhaustion.pl diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build index e71e95c6297..94cc9d0a112 100644 --- a/src/test/subscription/meson.build +++ b/src/test/subscription/meson.build @@ -48,6 +48,7 @@ tests += { 't/036_sequences.pl', 't/037_except.pl', 't/038_walsnd_shutdown_timeout.pl', + 't/039_sequencesync_lock_exhaustion.pl', 't/100_bugs.pl', ], }, diff --git a/src/test/subscription/t/039_sequencesync_lock_exhaustion.pl b/src/test/subscription/t/039_sequencesync_lock_exhaustion.pl new file mode 100644 index 00000000000..e00efaf493a --- /dev/null +++ b/src/test/subscription/t/039_sequencesync_lock_exhaustion.pl @@ -0,0 +1,120 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test that the sequencesync worker's initial "gathering" scan can exhaust the +# subscriber's shared lock table. +# +# LogicalRepSyncSequences() (sequencesync.c) walks every srsubstate='i' relation +# of the subscription and opens each one with try_table_open(..., RowExclusiveLock) +# in a *single* transaction, retaining all of those locks until that transaction +# commits. With enough sequences in INIT state and a small shared lock table, +# that one transaction runs out of lock-table space and fails with: +# +# out of shared memory +# HINT: You might need to increase "max_locks_per_transaction". + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Enough sequences that the gathering scan's per-relation RowExclusiveLocks +# overflow the small shared lock table configured further below. With that +# configuration the table holds 210 entries and each backend has only 16 +# fast-path slots, so 300 sequences overflow it by a wide margin. +my $nseqs = 300; + +# Initialize publisher node +my $node_publisher = PostgreSQL::Test::Cluster->new('publisher'); +$node_publisher->init(allows_streaming => 'logical'); +$node_publisher->start; + +# Initialize subscriber node. It starts with the default (large) lock table so +# the initial synchronization of all sequences succeeds; the lock table is +# shrunk afterwards, before the REFRESH that is expected to fail. +my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber'); +$node_subscriber->init; +$node_subscriber->start; + +# Create the sequences on both sides. +my $create_seqs = + join('', map { "CREATE SEQUENCE regress_s$_;\n" } (1 .. $nseqs)); +$node_publisher->safe_psql('postgres', $create_seqs); +$node_subscriber->safe_psql('postgres', $create_seqs); + +# Advance each sequence on the publisher so there is real state to copy. +$node_publisher->safe_psql('postgres', + join('', map { "SELECT nextval('regress_s$_');\n" } (1 .. $nseqs))); + +# Setup logical replication. +my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres'; +$node_publisher->safe_psql('postgres', + "CREATE PUBLICATION regress_seq_pub FOR ALL SEQUENCES"); +$node_subscriber->safe_psql('postgres', + "CREATE SUBSCRIPTION regress_seq_sub CONNECTION '$publisher_connstr' PUBLICATION regress_seq_pub" +); + +# The initial sync of all sequences must succeed with the default lock table. +my $synced_query = + "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r');"; +$node_subscriber->poll_query_until('postgres', $synced_query) + or die "Timed out while waiting for subscriber to synchronize sequences"; + +is( $node_subscriber->safe_psql( + 'postgres', + "SELECT count(*) FROM pg_subscription_rel WHERE srsubstate = 'r'"), + $nseqs, + "all $nseqs sequences initially synchronized"); + +########## +# Shrink the subscriber's shared lock table below the number of sequences, then +# REFRESH SEQUENCES so the gathering scan tries to lock all of them in one +# transaction and runs out of shared memory. +########## + +# NLOCKENTS() = max_locks_per_transaction * (MaxBackends + max_prepared_xacts). +# With the settings below: +# MaxBackends = max_connections (10) + autovacuum_worker_slots (1) +# + max_worker_processes (8) + max_wal_senders (0) +# + NUM_SPECIAL_WORKER_PROCS (2) +# = 21 +# max_prepared_xacts = 0 +# => NLOCKENTS() = 10 * 21 = 210, well below $nseqs (300). +$node_subscriber->append_conf( + 'postgresql.conf', qq( +max_locks_per_transaction = 10 +max_connections = 10 +autovacuum_worker_slots = 1 +max_worker_processes = 8 +max_wal_senders = 0 +)); +$node_subscriber->restart; + +my $log_offset = -s $node_subscriber->logfile; + +# REFRESH SEQUENCES resets every sequence to INIT. Note that this foreground +# command only updates catalog rows and does not lock the sequence relations, +# so it succeeds; it is the background sequencesync worker's gathering scan that +# holds a RowExclusiveLock on every INIT sequence at once and overflows the +# shared lock table. +$node_subscriber->safe_psql('postgres', + "ALTER SUBSCRIPTION regress_seq_sub REFRESH SEQUENCES"); + +$node_subscriber->wait_for_log(qr/out of shared memory/, $log_offset); +$node_subscriber->wait_for_log( + qr/You might need to increase "max_locks_per_transaction"/, $log_offset); + +pass('sequencesync gathering scan exhausts the shared lock table'); + +# The scan fails before any sequence is marked READY, so synchronization makes +# no progress: every sequence is still in a non-ready state. +is( $node_subscriber->safe_psql( + 'postgres', + "SELECT count(*) FROM pg_subscription_rel WHERE srsubstate = 'r'"), + '0', + 'no sequence reaches READY while the lock table is exhausted'); + +$node_subscriber->stop('fast'); +$node_publisher->stop('fast'); + +done_testing(); -- 2.50.1 (Apple Git-155)