From 8fcba03aabc2defbad75628b337fa0c651039a50 Mon Sep 17 00:00:00 2001 From: Joel Jacobson Date: Tue, 19 May 2026 10:55:50 -0700 Subject: [PATCH 2/3] Test LISTEN startup notification for already-seen work Add an injection-point isolation test that pauses a first LISTEN after the pending listen action has been applied, but before the command returns to the client. A concurrent transaction inserts a row and sends a NOTIFY while the listener is paused. When LISTEN returns, the listener receives one notification and its initial table scan sees the same row. This demonstrates the harmless false positive described by the LISTEN documentation: applications may observe the same work through both the startup scan and the notification stream, and should tolerate that. --- src/backend/commands/async.c | 3 ++ src/test/modules/injection_points/Makefile | 1 + .../async-notify-listen-false-positive.out | 21 +++++++++ src/test/modules/injection_points/meson.build | 1 + .../async-notify-listen-false-positive.spec | 43 +++++++++++++++++++ 5 files changed, 69 insertions(+) create mode 100644 src/test/modules/injection_points/expected/async-notify-listen-false-positive.out create mode 100644 src/test/modules/injection_points/specs/async-notify-listen-false-positive.spec diff --git a/src/backend/commands/async.c b/src/backend/commands/async.c index cefd5297a73..bd7eff7f305 100644 --- a/src/backend/commands/async.c +++ b/src/backend/commands/async.c @@ -1394,6 +1394,9 @@ AtCommit_Notify(void) /* Apply staged listen/unlisten changes */ ApplyPendingListenActions(true); + if (pendingActions != NULL) + INJECTION_POINT("async-notify-after-listen-commit", NULL); + /* If no longer listening to anything, get out of listener array */ if (amRegisteredListener && LocalChannelTableIsEmpty()) asyncQueueUnregister(); diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index 37c1b1cffb6..dcf442df9ba 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -13,6 +13,7 @@ REGRESS = injection_points hashagg reindex_conc vacuum REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress ISOLATION = basic \ + async-notify-listen-false-positive \ async-notify-listen-startup \ inplace \ repack \ diff --git a/src/test/modules/injection_points/expected/async-notify-listen-false-positive.out b/src/test/modules/injection_points/expected/async-notify-listen-false-positive.out new file mode 100644 index 00000000000..4cab951ca55 --- /dev/null +++ b/src/test/modules/injection_points/expected/async-notify-listen-false-positive.out @@ -0,0 +1,21 @@ +Parsed test spec with 3 sessions + +starting permutation: listen notify wake inspect +step listen: LISTEN race; +step notify: + INSERT INTO notify_queue VALUES (1); + NOTIFY race, '1'; + +step wake: + SELECT FROM injection_points_detach('async-notify-after-listen-commit'); + SELECT FROM injection_points_wakeup('async-notify-after-listen-commit'); + +step listen: <... completed> +listener: NOTIFY "race" with payload "1" from notifier +step inspect: SELECT id FROM notify_queue ORDER BY id; +id +-- + 1 +(1 row) + +step wake: <... completed> diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index 61a68bcfe15..8a91b0a41aa 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -44,6 +44,7 @@ tests += { 'isolation': { 'specs': [ 'basic', + 'async-notify-listen-false-positive', 'async-notify-listen-startup', 'inplace', 'repack', diff --git a/src/test/modules/injection_points/specs/async-notify-listen-false-positive.spec b/src/test/modules/injection_points/specs/async-notify-listen-false-positive.spec new file mode 100644 index 00000000000..ad31c37acb6 --- /dev/null +++ b/src/test/modules/injection_points/specs/async-notify-listen-false-positive.spec @@ -0,0 +1,43 @@ +# Test harmless duplicate notification during LISTEN startup. +# +# A notification can arrive for work that the first database inspection after +# LISTEN already observes. Applications should inspect database state after +# LISTEN returns and tolerate a few notifications for rows already seen. + +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE notify_queue(id int primary key); +} + +teardown +{ + DROP TABLE notify_queue; + DROP EXTENSION injection_points; +} + +session listener +setup +{ + SELECT FROM injection_points_set_local(); + SELECT FROM injection_points_attach('async-notify-after-listen-commit', 'wait'); +} +step listen { LISTEN race; } +step inspect { SELECT id FROM notify_queue ORDER BY id; } +teardown { UNLISTEN *; } + +session notifier +step notify +{ + INSERT INTO notify_queue VALUES (1); + NOTIFY race, '1'; +} + +session controller +step wake +{ + SELECT FROM injection_points_detach('async-notify-after-listen-commit'); + SELECT FROM injection_points_wakeup('async-notify-after-listen-commit'); +} + +permutation listen notify wake(listen) inspect -- 2.52.0