From 00b372089d9c01c66b5e919187238ffb6ce45f0d Mon Sep 17 00:00:00 2001 From: Arseniy Mukhin 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