From 16c94f3d118a4da02b37b9a45d2abafda6b984f4 Mon Sep 17 00:00:00 2001 From: Joel Jacobson Date: Tue, 19 May 2026 10:44:46 -0700 Subject: [PATCH 1/3] Test missed LISTEN startup notification Add an injection-point isolation test that pauses a first LISTEN before AtCommit_Notify() applies the pending listen action. A concurrent NOTIFY in that window should be delivered when LISTEN returns. This is distinct from the setup race documented in listen.sgml. That race can produce false positives: a new listener can receive notifications for changes it already saw during its initial database inspection. Such extra notifications are harmless. The race tested here is a false negative. The listener has already registered its queue position, and its channel table entry is visible to SignalBackends(). However, AtCommit_Notify() has not yet marked it listening=true, so a notification committed after that point can be skipped and lost entirely. Before the fix, this test fails by missing that notification. The test would have passed before the LISTEN/NOTIFY rework in 282b1cd, which introduced the shared channel map and direct advancement. --- src/backend/commands/async.c | 4 ++ src/test/modules/injection_points/Makefile | 1 + .../expected/async-notify-listen-startup.out | 18 +++++++++ src/test/modules/injection_points/meson.build | 1 + .../specs/async-notify-listen-startup.spec | 38 +++++++++++++++++++ 5 files changed, 62 insertions(+) create mode 100644 src/test/modules/injection_points/expected/async-notify-listen-startup.out create mode 100644 src/test/modules/injection_points/specs/async-notify-listen-startup.spec diff --git a/src/backend/commands/async.c b/src/backend/commands/async.c index db6a9a6561b..cefd5297a73 100644 --- a/src/backend/commands/async.c +++ b/src/backend/commands/async.c @@ -184,6 +184,7 @@ #include "utils/builtins.h" #include "utils/dsa.h" #include "utils/guc_hooks.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #include "utils/ps_status.h" #include "utils/snapmgr.h" @@ -1387,6 +1388,9 @@ AtCommit_Notify(void) if (Trace_notify) elog(DEBUG1, "AtCommit_Notify"); + if (pendingActions != NULL) + INJECTION_POINT("async-notify-before-listen-commit", NULL); + /* Apply staged listen/unlisten changes */ ApplyPendingListenActions(true); diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index c01d2fb095c..37c1b1cffb6 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-startup \ inplace \ repack \ repack_temporal \ diff --git a/src/test/modules/injection_points/expected/async-notify-listen-startup.out b/src/test/modules/injection_points/expected/async-notify-listen-startup.out new file mode 100644 index 00000000000..d65e5015cff --- /dev/null +++ b/src/test/modules/injection_points/expected/async-notify-listen-startup.out @@ -0,0 +1,18 @@ +Parsed test spec with 3 sessions + +starting permutation: listen notify wake check +step listen: LISTEN race; +step notify: NOTIFY race, 'payload'; +step wake: + SELECT FROM injection_points_detach('async-notify-before-listen-commit'); + SELECT FROM injection_points_wakeup('async-notify-before-listen-commit'); + +step listen: <... completed> +listener: NOTIFY "race" with payload "payload" from notifier +step check: SELECT 1 AS x; +x +- +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 59dba1cb023..61a68bcfe15 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-startup', 'inplace', 'repack', 'repack_temporal', diff --git a/src/test/modules/injection_points/specs/async-notify-listen-startup.spec b/src/test/modules/injection_points/specs/async-notify-listen-startup.spec new file mode 100644 index 00000000000..29832a514d0 --- /dev/null +++ b/src/test/modules/injection_points/specs/async-notify-listen-startup.spec @@ -0,0 +1,38 @@ +# Test LISTEN/NOTIFY startup behavior while committing the first LISTEN. +# +# A first LISTEN registers the backend's queue position before the transaction +# becomes visible, then commits the local listen state later in AtCommit_Notify. +# A concurrent NOTIFY in that window must still wake the listener, so the +# notification is delivered after LISTEN returns. + +setup +{ + CREATE EXTENSION injection_points; +} + +teardown +{ + DROP EXTENSION injection_points; +} + +session listener +setup +{ + SELECT FROM injection_points_set_local(); + SELECT FROM injection_points_attach('async-notify-before-listen-commit', 'wait'); +} +step listen { LISTEN race; } +step check { SELECT 1 AS x; } +teardown { UNLISTEN *; } + +session notifier +step notify { NOTIFY race, 'payload'; } + +session controller +step wake +{ + SELECT FROM injection_points_detach('async-notify-before-listen-commit'); + SELECT FROM injection_points_wakeup('async-notify-before-listen-commit'); +} + +permutation listen notify wake(listen) check -- 2.52.0