Skip to content

Commit d37776e

Browse files
committed
Make timeout.c more robust against missed timer interrupts.
Commit 09cf1d5 taught schedule_alarm() to not do anything if the next requested event is after when we expect the next interrupt to fire. However, if somehow an interrupt gets lost, we'll continue to not do anything indefinitely, even after the "next interrupt" time is obviously in the past. Thus, one missed interrupt can break timeout scheduling for the life of the session. Michael Harris reported a scenario where a bug in a user-defined function caused this to happen, so you don't even need to assume kernel bugs exist to think this is worth fixing. We can make things more robust at little cost by detecting the case where signal_due_at is before "now" and forcing a new setitimer call to occur. This isn't a completely bulletproof fix of course; but in our typical usage pattern where we frequently set timeouts and clear them before they are reached, the interrupt will get re-enabled after at most one timeout interval, which with a little luck will be before we really need it. While here, let's mark signal_due_at as volatile, since the signal handler can both examine and set it. I'm not sure there's any actual risk given that signal_pending is already volatile, but it's surely questionable. Backpatch to v14 where this logic came in. Michael Harris and Tom Lane Discussion: https://postgr.es/m/CADofcAWbMrvgwSMqO4iG_iD3E2v8ZUrC-_crB41my=VMM02-CA@mail.gmail.com
1 parent 9cd28c2 commit d37776e

File tree

1 file changed

+16
-3
lines changed

1 file changed

+16
-3
lines changed

src/backend/utils/misc/timeout.c

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,12 @@ static volatile sig_atomic_t alarm_enabled = false;
7171

7272
/*
7373
* State recording if and when we next expect the interrupt to fire.
74+
* (signal_due_at is valid only when signal_pending is true.)
7475
* Note that the signal handler will unconditionally reset signal_pending to
7576
* false, so that can change asynchronously even when alarm_enabled is false.
7677
*/
7778
static volatile sig_atomic_t signal_pending = false;
78-
static TimestampTz signal_due_at = 0; /* valid only when signal_pending */
79+
static volatile TimestampTz signal_due_at = 0;
7980

8081

8182
/*****************************************************************************
@@ -217,10 +218,22 @@ schedule_alarm(TimestampTz now)
217218

218219
MemSet(&timeval, 0, sizeof(struct itimerval));
219220

221+
/*
222+
* If we think there's a signal pending, but current time is more than
223+
* 10ms past when the signal was due, then assume that the timeout
224+
* request got lost somehow; clear signal_pending so that we'll reset
225+
* the interrupt request below. (10ms corresponds to the worst-case
226+
* timeout granularity on modern systems.) It won't hurt us if the
227+
* interrupt does manage to fire between now and when we reach the
228+
* setitimer() call.
229+
*/
230+
if (signal_pending && now > signal_due_at + 10 * 1000)
231+
signal_pending = false;
232+
220233
/*
221234
* Get the time remaining till the nearest pending timeout. If it is
222-
* negative, assume that we somehow missed an interrupt, and force
223-
* signal_pending off. This gives us a chance to recover if the
235+
* negative, assume that we somehow missed an interrupt, and clear
236+
* signal_pending. This gives us another chance to recover if the
224237
* kernel drops a timeout request for some reason.
225238
*/
226239
nearest_timeout = active_timeouts[0]->fin_time;

0 commit comments

Comments
 (0)