From 5c1fa5177b53779bcb8f200b19d66cb61a8b4208 Mon Sep 17 00:00:00 2001 From: Joel Jacobson Date: Tue, 19 May 2026 07:36:25 -0700 Subject: [PATCH 3/3] Fix LISTEN startup race with direct advancement LISTEN creates its shared channel-map entry before commit, with listening=false until AtCommit_Notify() applies the staged action. A concurrent NOTIFY can see that commit in the window before the flag is flipped. SignalBackends() treated that backend as not interested, so direct advancement could move its queue pointer past the notification. Do not test listeners[j].listening when deciding whom to wake. A false value can mean a LISTEN that has registered its queue position, but has not yet run AtCommit_Notify(). Waking such a backend is harmless if it aborts; advancing it here could make it miss a notification after commit. The preceding missed-notification test now passes with the fix. --- src/backend/commands/async.c | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/backend/commands/async.c b/src/backend/commands/async.c index bd7eff7f305..f1bd278bd19 100644 --- a/src/backend/commands/async.c +++ b/src/backend/commands/async.c @@ -114,11 +114,11 @@ * If the current transaction has executed any LISTEN/UNLISTEN actions, * PreCommit_Notify() prepares to commit those. For LISTEN, it * pre-allocates entries in both the per-backend localChannelTable and the - * shared globalChannelTable (with listening=false so that these entries - * are no-ops for the moment). It also records the final per-channel - * intent in pendingListenActions, so post-commit/abort processing can - * apply that in a single step. Since all these allocations happen before - * committing to clog, we can safely abort the transaction on failure. + * shared globalChannelTable (with listening=false to mark these entries + * as staged). It also records the final per-channel intent in + * pendingListenActions, so post-commit/abort processing can apply that in + * a single step. Since all these allocations happen before committing to + * clog, we can safely abort the transaction on failure. * * After commit, AtCommit_Notify() runs through pendingListenActions and * updates the backend's per-channel listening flags to activate or @@ -2311,9 +2311,6 @@ SignalBackends(void) int32 pid; QueuePosition pos; - if (!listeners[j].listening) - continue; /* ignore not-yet-committed listeners */ - i = listeners[j].procNo; if (QUEUE_BACKEND_WAKEUP_PENDING(i)) @@ -2327,6 +2324,14 @@ SignalBackends(void) Assert(pid != InvalidPid); + /* + * Do not test listeners[j].listening here. A false value can + * mean a LISTEN that has registered its queue position, but has + * not yet run AtCommit_Notify(). Waking such a backend is + * harmless if it aborts; advancing it here could make it miss a + * notification after commit. + */ + QUEUE_BACKEND_WAKEUP_PENDING(i) = true; signalPids[count] = pid; signalProcnos[count] = i; -- 2.52.0