v5-0002-tap-tests.patch

text/x-patch

Filename: v5-0002-tap-tests.patch
Type: text/x-patch
Part: 1
Message: Re: LISTEN/NOTIFY bug: VACUUM sets frozenxid past a xid in async queue

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: format-patch
Series: patch v5-0002
Subject: tap tests
File+
src/backend/commands/async.c 5 0
src/include/storage/lmgr.h 1 1
src/test/modules/test_listen_notify/meson.build 5 1
src/test/modules/test_listen_notify/t/002_aborted_tx_notifies.pl 21 11
src/test/modules/test_listen_notify/t/003_postpone_in_progress_aborted_notifications.pl 114 0
From 00b372089d9c01c66b5e919187238ffb6ce45f0d Mon Sep 17 00:00:00 2001
From: Arseniy Mukhin <arseniy.mukhin.dev@gmail.com>
Date: Fri, 26 Sep 2025 13:17:59 +0300
Subject: [PATCH v5 2/2] tap tests

---
 src/backend/commands/async.c                  |   5 +
 src/include/storage/lmgr.h                    |   2 +-
 .../modules/test_listen_notify/meson.build    |   6 +-
 .../t/002_aborted_tx_notifies.pl              |  32 +++--
 ...tpone_in_progress_aborted_notifications.pl | 114 ++++++++++++++++++
 5 files changed, 146 insertions(+), 13 deletions(-)
 create mode 100644 src/test/modules/test_listen_notify/t/003_postpone_in_progress_aborted_notifications.pl

diff --git a/src/backend/commands/async.c b/src/backend/commands/async.c
index 4e7c0e16d57..f6f8fa6134c 100644
--- a/src/backend/commands/async.c
+++ b/src/backend/commands/async.c
@@ -164,6 +164,7 @@
 #include "utils/ps_status.h"
 #include "utils/snapmgr.h"
 #include "utils/timestamp.h"
+#include "utils/injection_point.h"
 
 
 /*
@@ -2138,6 +2139,8 @@ asyncQueueProcessPageEntries(volatile QueuePosition *current,
 				 * because our transaction cannot (yet) have queued any
 				 * messages.
 				 */
+
+				INJECTION_POINT("listen-notify-in-progress-notification", NULL);
 				*current = thisentry;
 				reachedStop = true;
 				break;
@@ -2497,6 +2500,8 @@ asyncQueueRollbackNotifications(void)
 	QueuePosition current = atAbortInfo->previousHead;
 	QueuePosition head = atAbortInfo->head;
 
+	INJECTION_POINT("listen-notify-notifications-rollback", NULL);
+
 	/*
 	 * Iterates from the position saved at the beginning of the transaction
 	 * (previousHead) to the current head of the queue. We do this to mark all
diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h
index c119c8f4ded..3d34e61772d 100644
--- a/src/include/storage/lmgr.h
+++ b/src/include/storage/lmgr.h
@@ -51,7 +51,7 @@ extern bool CheckRelationLockedByMe(Relation relation, LOCKMODE lockmode,
 extern bool CheckRelationOidLockedByMe(Oid relid, LOCKMODE lockmode,
 									   bool orstronger);
 extern bool CheckSharedObjectLockedByMe(Oid classid, LOCKMODE lockmode,
-									   bool orstronger);
+										bool orstronger);
 
 extern bool LockHasWaitersRelation(Relation relation, LOCKMODE lockmode);
 
diff --git a/src/test/modules/test_listen_notify/meson.build b/src/test/modules/test_listen_notify/meson.build
index a68052cd353..565f4ec8ef0 100644
--- a/src/test/modules/test_listen_notify/meson.build
+++ b/src/test/modules/test_listen_notify/meson.build
@@ -5,9 +5,13 @@ tests += {
   'sd': meson.current_source_dir(),
   'bd': meson.current_build_dir(),
   'tap': {
+    'env': {
+      'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+    },
     'tests': [
       't/001_xid_freeze.pl',
-      't/002_aborted_tx_notifies.pl'
+      't/002_aborted_tx_notifies.pl',
+      't/003_postpone_in_progress_aborted_notifications.pl'
     ],
   },
 }
diff --git a/src/test/modules/test_listen_notify/t/002_aborted_tx_notifies.pl b/src/test/modules/test_listen_notify/t/002_aborted_tx_notifies.pl
index 17fcb4b786e..74c4ae9fa9d 100644
--- a/src/test/modules/test_listen_notify/t/002_aborted_tx_notifies.pl
+++ b/src/test/modules/test_listen_notify/t/002_aborted_tx_notifies.pl
@@ -7,15 +7,15 @@ use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
 use Test::More;
 
-my $node = PostgreSQL::Test::Cluster->new('node');
-$node->init;
-$node->start;
-
 # Test checks that listeners do not receive notifications from aborted
 # transaction even if notifications have been added to the listen/notify
 # queue. To reproduce it we use the fact that serializable conflicts
 # are checked after tx adds notifications to the queue.
 
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
 # Setup
 $node->safe_psql('postgres', 'CREATE TABLE t1 (a bigserial);');
 
@@ -29,7 +29,8 @@ $psql_session1->query_safe("
 	BEGIN ISOLATION LEVEL SERIALIZABLE;
 	SELECT * FROM t1;
 	INSERT INTO t1 DEFAULT VALUES;
-	NOTIFY ch,'committed';
+	NOTIFY ch,'committed_0';
+	NOTIFY ch,'committed_1';
 ");
 
 # Session2. Start SERIALIZABLE tx, add a notification and introduce a conflict
@@ -39,9 +40,15 @@ $psql_session2->query_safe("
 	BEGIN ISOLATION LEVEL SERIALIZABLE;
 	SELECT * FROM t1;
 	INSERT INTO t1 DEFAULT VALUES;
-	NOTIFY ch,'aborted';
 ");
 
+# Send notifications that should not be eventually delivered, as session2
+# transaction will be aborted.
+my $message = 'aborted_' . 'a' x 1000;
+for (my $i = 0; $i < 10; $i++) {
+    $psql_session2->query_safe("NOTIFY ch, '$i$message'");
+}
+
 # Session1 should be committed successfully. Listeners must receive session1
 # notifications.
 $psql_session1->query_safe("COMMIT;");
@@ -51,16 +58,19 @@ $psql_session1->query_safe("COMMIT;");
 # listeners should not receive session2 notifications.
 $psql_session2->query("COMMIT;");
 
-# send another notification after aborted
-$node->safe_psql('postgres', "NOTIFY ch, 'next_committed';");
+# send more notifications after aborted
+$node->safe_psql('postgres', "NOTIFY ch, 'committed_2';");
+$node->safe_psql('postgres', "NOTIFY ch, 'committed_3';");
 
 # fetch notifications
 my $res = $psql_listener->query_safe('begin; commit;');
 
 # check received notifications
 my @lines = split('\n', $res);
-is(@lines, 2, 'received all committed notifications');
-like($lines[0], qr/Asynchronous notification "ch" with payload "committed" received/);
-like($lines[1], qr/Asynchronous notification "ch" with payload "next_committed" received/);
+is(@lines, 4, 'received all committed notifications');
+for (my $i = 0; $i < 4; $i++) {
+    like($lines[$i], qr/Asynchronous notification "ch" with payload "committed_$i" received/);
+}
+
 
 done_testing();
diff --git a/src/test/modules/test_listen_notify/t/003_postpone_in_progress_aborted_notifications.pl b/src/test/modules/test_listen_notify/t/003_postpone_in_progress_aborted_notifications.pl
new file mode 100644
index 00000000000..b53e6a1cdaa
--- /dev/null
+++ b/src/test/modules/test_listen_notify/t/003_postpone_in_progress_aborted_notifications.pl
@@ -0,0 +1,114 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+use File::Path qw(mkpath);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Test checks that listeners do not receive notifications from aborted
+# even if the see such notifications in the queue right before the moment
+# aborted transaction marks its notifications as 'committed = false'. To
+# reproduce it we use the fact that serializable conflicts are checked after
+# tx adds notifications to the queue.
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+    plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+# Check if the extension injection_points is available, as it may be
+# possible that this script is run with installcheck, where the module
+# would not be installed by default.
+if (!$node->check_extension('injection_points'))
+{
+    plan skip_all => 'Extension injection_points not installed';
+}
+
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points');
+
+# Setup
+$node->safe_psql('postgres', 'CREATE TABLE t1 (a bigserial);');
+
+# Injection points setup
+$node->safe_psql('postgres',"SELECT injection_points_attach('listen-notify-notifications-rollback', 'wait')");
+$node->safe_psql('postgres',"SELECT injection_points_attach('listen-notify-in-progress-notification', 'notice')");
+
+# Session1. Start SERIALIZABLE tx and add a notification.
+my $psql_session1 = $node->background_psql('postgres');
+$psql_session1->query_safe("
+	BEGIN ISOLATION LEVEL SERIALIZABLE;
+	SELECT * FROM t1;
+	INSERT INTO t1 DEFAULT VALUES;
+	NOTIFY ch,'committed';
+");
+
+# Session2. Start SERIALIZABLE tx, add a notification and introduce a conflict
+# with session1.
+my $psql_session2 = $node->background_psql('postgres', on_error_stop => 0);
+$psql_session2->query_safe("
+	BEGIN ISOLATION LEVEL SERIALIZABLE;
+	SELECT * FROM t1;
+	INSERT INTO t1 DEFAULT VALUES;
+	NOTIFY ch,'aborted';
+");
+
+# Session1 should be committed successfully and publish notification.
+$psql_session1->query_safe("COMMIT;");
+
+# Session2 should be aborted due to the conflict with session1. Transaction
+# is aborted after adding notifications to the listen/notify queue, so we have notification of
+# aborted transaction in the queue. Session2 should start sleeping on injection point right before
+# marking its notifications as 'committed = false'
+$psql_session2->query_until(
+    qr/start/, q(
+    \echo start
+    COMMIT;
+));
+
+$node->wait_for_event('client backend', 'listen-notify-notifications-rollback');
+
+# Setup listener
+my $psql_listener = $node->background_psql('postgres');
+$psql_listener->query_safe("SET log_min_messages = 'NOTICE'");
+
+# At the moment listener should skip the first committed notification (as it was committed before we started listening)
+# and stop on the first aborted notification as notify transaction is still in progress and backend is sleeping.
+my $log_offset = -s $node->logfile;
+my $res = $psql_listener->query('LISTEN ch;');
+
+# Check that we touched session2's pending notification and triggered injection point
+$node->wait_for_log(qr/notice triggered for injection point listen-notify-in-progress-notification/, $log_offset);
+
+# Check listener has no notifications
+my @lines = split('\n', $res);
+is(@lines, 0,  'received no notifications');
+
+# Wakeup backend with aborted transaction
+$node->safe_psql('postgres',"SELECT injection_points_wakeup('listen-notify-notifications-rollback');");
+
+# Send another notification after aborted
+$node->safe_psql('postgres', "NOTIFY ch, 'next_committed'");
+
+# Clean stderr as workaround of the current bug with background psql hanging
+$psql_listener->{stderr} = "";
+
+# Fetch the rest notifications
+$res = $psql_listener->query_safe('begin; commit;');
+
+
+# Now aborted transaction is completed, so listener must skip aborted notifications and get next_committed
+@lines = split('\n', $res);
+is(@lines, 1,  'received all committed notifications');
+like($lines[0], qr/Asynchronous notification "ch" with payload "next_committed" received/);
+
+# Injection points cleanup
+$node->safe_psql('postgres',"SELECT injection_points_detach('listen-notify-notifications-rollback');");
+$node->safe_psql('postgres',"SELECT injection_points_detach('listen-notify-in-progress-notification');");
+
+done_testing();
-- 
2.43.0