Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit d3aa114

Browse files
committed
Doc: improve discussion of race conditions involved in LISTEN.
The user docs didn't really explain how to use LISTEN safely, so clarify that. Also clean up some fuzzy-headed explanations in comments. No code changes. Discussion: https://postgr.es/m/3ac7f397-4d5f-be8e-f354-440020675694@gmail.com
1 parent 6b802cf commit d3aa114

File tree

2 files changed

+65
-36
lines changed

2 files changed

+65
-36
lines changed

doc/src/sgml/ref/listen.sgml

+24-7
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,6 @@ LISTEN <replaceable class="parameter">channel</replaceable>
6363
<command>LISTEN</command> or <command>UNLISTEN</command> directly. See the
6464
documentation for the interface you are using for more details.
6565
</para>
66-
67-
<para>
68-
<xref linkend="sql-notify"/>
69-
contains a more extensive
70-
discussion of the use of <command>LISTEN</command> and
71-
<command>NOTIFY</command>.
72-
</para>
7366
</refsect1>
7467

7568
<refsect1>
@@ -96,10 +89,34 @@ LISTEN <replaceable class="parameter">channel</replaceable>
9689
within a transaction that later rolls back, the set of notification
9790
channels being listened to is unchanged.
9891
</para>
92+
9993
<para>
10094
A transaction that has executed <command>LISTEN</command> cannot be
10195
prepared for two-phase commit.
10296
</para>
97+
98+
<para>
99+
There is a race condition when first setting up a listening session:
100+
if concurrently-committing transactions are sending notify events,
101+
exactly which of those will the newly listening session receive?
102+
The answer is that the session will receive all events committed after
103+
an instant during the transaction's commit step. But that is slightly
104+
later than any database state that the transaction could have observed
105+
in queries. This leads to the following rule for
106+
using <command>LISTEN</command>: first execute (and commit!) that
107+
command, then in a new transaction inspect the database state as needed
108+
by the application logic, then rely on notifications to find out about
109+
subsequent changes to the database state. The first few received
110+
notifications might refer to updates already observed in the initial
111+
database inspection, but this is usually harmless.
112+
</para>
113+
114+
<para>
115+
<xref linkend="sql-notify"/>
116+
contains a more extensive
117+
discussion of the use of <command>LISTEN</command> and
118+
<command>NOTIFY</command>.
119+
</para>
103120
</refsect1>
104121

105122
<refsect1>

src/backend/commands/async.c

+41-29
Original file line numberDiff line numberDiff line change
@@ -1112,14 +1112,12 @@ Exec_ListenPreCommit(void)
11121112
amRegisteredListener = true;
11131113

11141114
/*
1115-
* Try to move our pointer forward as far as possible. This will skip over
1116-
* already-committed notifications. Still, we could get notifications that
1117-
* have already committed before we started to LISTEN.
1118-
*
1119-
* Note that we are not yet listening on anything, so we won't deliver any
1120-
* notification to the frontend. Also, although our transaction might
1121-
* have executed NOTIFY, those message(s) aren't queued yet so we can't
1122-
* see them in the queue.
1115+
* Try to move our pointer forward as far as possible. This will skip
1116+
* over already-committed notifications, which we want to do because they
1117+
* might be quite stale. Note that we are not yet listening on anything,
1118+
* so we won't deliver such notifications to our frontend. Also, although
1119+
* our transaction might have executed NOTIFY, those message(s) aren't
1120+
* queued yet so we won't skip them here.
11231121
*/
11241122
if (!QUEUE_POS_EQUAL(max, head))
11251123
asyncQueueReadAllNotifications();
@@ -1938,43 +1936,57 @@ asyncQueueReadAllNotifications(void)
19381936
return;
19391937
}
19401938

1941-
/* Get snapshot we'll use to decide which xacts are still in progress */
1942-
snapshot = RegisterSnapshot(GetLatestSnapshot());
1943-
19441939
/*----------
1945-
* Note that we deliver everything that we see in the queue and that
1946-
* matches our _current_ listening state.
1947-
* Especially we do not take into account different commit times.
1940+
* Get snapshot we'll use to decide which xacts are still in progress.
1941+
* This is trickier than it might seem, because of race conditions.
19481942
* Consider the following example:
19491943
*
19501944
* Backend 1: Backend 2:
19511945
*
19521946
* transaction starts
1947+
* UPDATE foo SET ...;
19531948
* NOTIFY foo;
19541949
* commit starts
1950+
* queue the notify message
19551951
* transaction starts
1956-
* LISTEN foo;
1957-
* commit starts
1952+
* LISTEN foo; -- first LISTEN in session
1953+
* SELECT * FROM foo WHERE ...;
19581954
* commit to clog
1955+
* commit starts
1956+
* add backend 2 to array of listeners
1957+
* advance to queue head (this code)
19591958
* commit to clog
19601959
*
1961-
* It could happen that backend 2 sees the notification from backend 1 in
1962-
* the queue. Even though the notifying transaction committed before
1963-
* the listening transaction, we still deliver the notification.
1960+
* Transaction 2's SELECT has not seen the UPDATE's effects, since that
1961+
* wasn't committed yet. Ideally we'd ensure that client 2 would
1962+
* eventually get transaction 1's notify message, but there's no way
1963+
* to do that; until we're in the listener array, there's no guarantee
1964+
* that the notify message doesn't get removed from the queue.
19641965
*
1965-
* The idea is that an additional notification does not do any harm, we
1966-
* just need to make sure that we do not miss a notification.
1966+
* Therefore the coding technique transaction 2 is using is unsafe:
1967+
* applications must commit a LISTEN before inspecting database state,
1968+
* if they want to ensure they will see notifications about subsequent
1969+
* changes to that state.
19671970
*
1968-
* It is possible that we fail while trying to send a message to our
1969-
* frontend (for example, because of encoding conversion failure).
1970-
* If that happens it is critical that we not try to send the same
1971-
* message over and over again. Therefore, we place a PG_TRY block
1972-
* here that will forcibly advance our backend position before we lose
1973-
* control to an error. (We could alternatively retake AsyncQueueLock
1974-
* and move the position before handling each individual message, but
1975-
* that seems like too much lock traffic.)
1971+
* What we do guarantee is that we'll see all notifications from
1972+
* transactions committing after the snapshot we take here.
1973+
* Exec_ListenPreCommit has already added us to the listener array,
1974+
* so no not-yet-committed messages can be removed from the queue
1975+
* before we see them.
19761976
*----------
19771977
*/
1978+
snapshot = RegisterSnapshot(GetLatestSnapshot());
1979+
1980+
/*
1981+
* It is possible that we fail while trying to send a message to our
1982+
* frontend (for example, because of encoding conversion failure). If
1983+
* that happens it is critical that we not try to send the same message
1984+
* over and over again. Therefore, we place a PG_TRY block here that will
1985+
* forcibly advance our queue position before we lose control to an error.
1986+
* (We could alternatively retake AsyncQueueLock and move the position
1987+
* before handling each individual message, but that seems like too much
1988+
* lock traffic.)
1989+
*/
19781990
PG_TRY();
19791991
{
19801992
bool reachedStop;

0 commit comments

Comments
 (0)