diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:15:05 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:15:05 +0000 |
commit | 46651ce6fe013220ed397add242004d764fc0153 (patch) | |
tree | 6e5299f990f88e60174a1d3ae6e48eedd2688b2b /src/backend/utils/misc/timeout.c | |
parent | Initial commit. (diff) | |
download | postgresql-14-46651ce6fe013220ed397add242004d764fc0153.tar.xz postgresql-14-46651ce6fe013220ed397add242004d764fc0153.zip |
Adding upstream version 14.5.upstream/14.5upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/backend/utils/misc/timeout.c')
-rw-r--r-- | src/backend/utils/misc/timeout.c | 779 |
1 files changed, 779 insertions, 0 deletions
diff --git a/src/backend/utils/misc/timeout.c b/src/backend/utils/misc/timeout.c new file mode 100644 index 0000000..b79df17 --- /dev/null +++ b/src/backend/utils/misc/timeout.c @@ -0,0 +1,779 @@ +/*------------------------------------------------------------------------- + * + * timeout.c + * Routines to multiplex SIGALRM interrupts for multiple timeout reasons. + * + * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/utils/misc/timeout.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include <sys/time.h> + +#include "miscadmin.h" +#include "storage/proc.h" +#include "utils/timeout.h" +#include "utils/timestamp.h" + + +/* Data about any one timeout reason */ +typedef struct timeout_params +{ + TimeoutId index; /* identifier of timeout reason */ + + /* volatile because these may be changed from the signal handler */ + volatile bool active; /* true if timeout is in active_timeouts[] */ + volatile bool indicator; /* true if timeout has occurred */ + + /* callback function for timeout, or NULL if timeout not registered */ + timeout_handler_proc timeout_handler; + + TimestampTz start_time; /* time that timeout was last activated */ + TimestampTz fin_time; /* time it is, or was last, due to fire */ +} timeout_params; + +/* + * List of possible timeout reasons in the order of enum TimeoutId. + */ +static timeout_params all_timeouts[MAX_TIMEOUTS]; +static bool all_timeouts_initialized = false; + +/* + * List of active timeouts ordered by their fin_time and priority. + * This list is subject to change by the interrupt handler, so it's volatile. + */ +static volatile int num_active_timeouts = 0; +static timeout_params *volatile active_timeouts[MAX_TIMEOUTS]; + +/* + * Flag controlling whether the signal handler is allowed to do anything. + * This is useful to avoid race conditions with the handler. Note in + * particular that this lets us make changes in the data structures without + * tediously disabling and re-enabling the timer signal. Most of the time, + * no interrupt would happen anyway during such critical sections, but if + * one does, this rule ensures it's safe. Leaving the signal enabled across + * multiple operations can greatly reduce the number of kernel calls we make, + * too. See comments in schedule_alarm() about that. + * + * We leave this "false" when we're not expecting interrupts, just in case. + */ +static volatile sig_atomic_t alarm_enabled = false; + +#define disable_alarm() (alarm_enabled = false) +#define enable_alarm() (alarm_enabled = true) + +/* + * State recording if and when we next expect the interrupt to fire. + * (signal_due_at is valid only when signal_pending is true.) + * Note that the signal handler will unconditionally reset signal_pending to + * false, so that can change asynchronously even when alarm_enabled is false. + */ +static volatile sig_atomic_t signal_pending = false; +static volatile TimestampTz signal_due_at = 0; + + +/***************************************************************************** + * Internal helper functions + * + * For all of these, it is caller's responsibility to protect them from + * interruption by the signal handler. Generally, call disable_alarm() + * first to prevent interruption, then update state, and last call + * schedule_alarm(), which will re-enable the signal handler if needed. + *****************************************************************************/ + +/* + * Find the index of a given timeout reason in the active array. + * If it's not there, return -1. + */ +static int +find_active_timeout(TimeoutId id) +{ + int i; + + for (i = 0; i < num_active_timeouts; i++) + { + if (active_timeouts[i]->index == id) + return i; + } + + return -1; +} + +/* + * Insert specified timeout reason into the list of active timeouts + * at the given index. + */ +static void +insert_timeout(TimeoutId id, int index) +{ + int i; + + if (index < 0 || index > num_active_timeouts) + elog(FATAL, "timeout index %d out of range 0..%d", index, + num_active_timeouts); + + Assert(!all_timeouts[id].active); + all_timeouts[id].active = true; + + for (i = num_active_timeouts - 1; i >= index; i--) + active_timeouts[i + 1] = active_timeouts[i]; + + active_timeouts[index] = &all_timeouts[id]; + + num_active_timeouts++; +} + +/* + * Remove the index'th element from the timeout list. + */ +static void +remove_timeout_index(int index) +{ + int i; + + if (index < 0 || index >= num_active_timeouts) + elog(FATAL, "timeout index %d out of range 0..%d", index, + num_active_timeouts - 1); + + Assert(active_timeouts[index]->active); + active_timeouts[index]->active = false; + + for (i = index + 1; i < num_active_timeouts; i++) + active_timeouts[i - 1] = active_timeouts[i]; + + num_active_timeouts--; +} + +/* + * Enable the specified timeout reason + */ +static void +enable_timeout(TimeoutId id, TimestampTz now, TimestampTz fin_time) +{ + int i; + + /* Assert request is sane */ + Assert(all_timeouts_initialized); + Assert(all_timeouts[id].timeout_handler != NULL); + + /* + * If this timeout was already active, momentarily disable it. We + * interpret the call as a directive to reschedule the timeout. + */ + if (all_timeouts[id].active) + remove_timeout_index(find_active_timeout(id)); + + /* + * Find out the index where to insert the new timeout. We sort by + * fin_time, and for equal fin_time by priority. + */ + for (i = 0; i < num_active_timeouts; i++) + { + timeout_params *old_timeout = active_timeouts[i]; + + if (fin_time < old_timeout->fin_time) + break; + if (fin_time == old_timeout->fin_time && id < old_timeout->index) + break; + } + + /* + * Mark the timeout active, and insert it into the active list. + */ + all_timeouts[id].indicator = false; + all_timeouts[id].start_time = now; + all_timeouts[id].fin_time = fin_time; + + insert_timeout(id, i); +} + +/* + * Schedule alarm for the next active timeout, if any + * + * We assume the caller has obtained the current time, or a close-enough + * approximation. (It's okay if a tick or two has passed since "now", or + * if a little more time elapses before we reach the kernel call; that will + * cause us to ask for an interrupt a tick or two later than the nearest + * timeout, which is no big deal. Passing a "now" value that's in the future + * would be bad though.) + */ +static void +schedule_alarm(TimestampTz now) +{ + if (num_active_timeouts > 0) + { + struct itimerval timeval; + TimestampTz nearest_timeout; + long secs; + int usecs; + + MemSet(&timeval, 0, sizeof(struct itimerval)); + + /* + * If we think there's a signal pending, but current time is more than + * 10ms past when the signal was due, then assume that the timeout + * request got lost somehow; clear signal_pending so that we'll reset + * the interrupt request below. (10ms corresponds to the worst-case + * timeout granularity on modern systems.) It won't hurt us if the + * interrupt does manage to fire between now and when we reach the + * setitimer() call. + */ + if (signal_pending && now > signal_due_at + 10 * 1000) + signal_pending = false; + + /* + * Get the time remaining till the nearest pending timeout. If it is + * negative, assume that we somehow missed an interrupt, and clear + * signal_pending. This gives us another chance to recover if the + * kernel drops a timeout request for some reason. + */ + nearest_timeout = active_timeouts[0]->fin_time; + if (now > nearest_timeout) + { + signal_pending = false; + /* force an interrupt as soon as possible */ + secs = 0; + usecs = 1; + } + else + { + TimestampDifference(now, nearest_timeout, + &secs, &usecs); + + /* + * It's possible that the difference is less than a microsecond; + * ensure we don't cancel, rather than set, the interrupt. + */ + if (secs == 0 && usecs == 0) + usecs = 1; + } + + timeval.it_value.tv_sec = secs; + timeval.it_value.tv_usec = usecs; + + /* + * We must enable the signal handler before calling setitimer(); if we + * did it in the other order, we'd have a race condition wherein the + * interrupt could occur before we can set alarm_enabled, so that the + * signal handler would fail to do anything. + * + * Because we didn't bother to disable the timer in disable_alarm(), + * it's possible that a previously-set interrupt will fire between + * enable_alarm() and setitimer(). This is safe, however. There are + * two possible outcomes: + * + * 1. The signal handler finds nothing to do (because the nearest + * timeout event is still in the future). It will re-set the timer + * and return. Then we'll overwrite the timer value with a new one. + * This will mean that the timer fires a little later than we + * intended, but only by the amount of time it takes for the signal + * handler to do nothing useful, which shouldn't be much. + * + * 2. The signal handler executes and removes one or more timeout + * events. When it returns, either the queue is now empty or the + * frontmost event is later than the one we looked at above. So we'll + * overwrite the timer value with one that is too soon (plus or minus + * the signal handler's execution time), causing a useless interrupt + * to occur. But the handler will then re-set the timer and + * everything will still work as expected. + * + * Since these cases are of very low probability (the window here + * being quite narrow), it's not worth adding cycles to the mainline + * code to prevent occasional wasted interrupts. + */ + enable_alarm(); + + /* + * If there is already an interrupt pending that's at or before the + * needed time, we need not do anything more. The signal handler will + * do the right thing in the first case, and re-schedule the interrupt + * for later in the second case. It might seem that the extra + * interrupt is wasted work, but it's not terribly much work, and this + * method has very significant advantages in the common use-case where + * we repeatedly set a timeout that we don't expect to reach and then + * cancel it. Instead of invoking setitimer() every time the timeout + * is set or canceled, we perform one interrupt and a re-scheduling + * setitimer() call at intervals roughly equal to the timeout delay. + * For example, with statement_timeout = 1s and a throughput of + * thousands of queries per second, this method requires an interrupt + * and setitimer() call roughly once a second, rather than thousands + * of setitimer() calls per second. + * + * Because of the possible passage of time between when we obtained + * "now" and when we reach setitimer(), the kernel's opinion of when + * to trigger the interrupt is likely to be a bit later than + * signal_due_at. That's fine, for the same reasons described above. + */ + if (signal_pending && nearest_timeout >= signal_due_at) + return; + + /* + * As with calling enable_alarm(), we must set signal_pending *before* + * calling setitimer(); if we did it after, the signal handler could + * trigger before we set it, leaving us with a false opinion that a + * signal is still coming. + * + * Other race conditions involved with setting/checking signal_pending + * are okay, for the reasons described above. One additional point is + * that the signal handler could fire after we set signal_due_at, but + * still before the setitimer() call. Then the handler could + * overwrite signal_due_at with a value it computes, which will be the + * same as or perhaps later than what we just computed. After we + * perform setitimer(), the net effect would be that signal_due_at + * gives a time later than when the interrupt will really happen; + * which is a safe situation. + */ + signal_due_at = nearest_timeout; + signal_pending = true; + + /* Set the alarm timer */ + if (setitimer(ITIMER_REAL, &timeval, NULL) != 0) + { + /* + * Clearing signal_pending here is a bit pro forma, but not + * entirely so, since something in the FATAL exit path could try + * to use timeout facilities. + */ + signal_pending = false; + elog(FATAL, "could not enable SIGALRM timer: %m"); + } + } +} + + +/***************************************************************************** + * Signal handler + *****************************************************************************/ + +/* + * Signal handler for SIGALRM + * + * Process any active timeout reasons and then reschedule the interrupt + * as needed. + */ +static void +handle_sig_alarm(SIGNAL_ARGS) +{ + int save_errno = errno; + + /* + * Bump the holdoff counter, to make sure nothing we call will process + * interrupts directly. No timeout handler should do that, but these + * failures are hard to debug, so better be sure. + */ + HOLD_INTERRUPTS(); + + /* + * SIGALRM is always cause for waking anything waiting on the process + * latch. + */ + SetLatch(MyLatch); + + /* + * Always reset signal_pending, even if !alarm_enabled, since indeed no + * signal is now pending. + */ + signal_pending = false; + + /* + * Fire any pending timeouts, but only if we're enabled to do so. + */ + if (alarm_enabled) + { + /* + * Disable alarms, just in case this platform allows signal handlers + * to interrupt themselves. schedule_alarm() will re-enable if + * appropriate. + */ + disable_alarm(); + + if (num_active_timeouts > 0) + { + TimestampTz now = GetCurrentTimestamp(); + + /* While the first pending timeout has been reached ... */ + while (num_active_timeouts > 0 && + now >= active_timeouts[0]->fin_time) + { + timeout_params *this_timeout = active_timeouts[0]; + + /* Remove it from the active list */ + remove_timeout_index(0); + + /* Mark it as fired */ + this_timeout->indicator = true; + + /* And call its handler function */ + this_timeout->timeout_handler(); + + /* + * The handler might not take negligible time (CheckDeadLock + * for instance isn't too cheap), so let's update our idea of + * "now" after each one. + */ + now = GetCurrentTimestamp(); + } + + /* Done firing timeouts, so reschedule next interrupt if any */ + schedule_alarm(now); + } + } + + RESUME_INTERRUPTS(); + + errno = save_errno; +} + + +/***************************************************************************** + * Public API + *****************************************************************************/ + +/* + * Initialize timeout module. + * + * This must be called in every process that wants to use timeouts. + * + * If the process was forked from another one that was also using this + * module, be sure to call this before re-enabling signals; else handlers + * meant to run in the parent process might get invoked in this one. + */ +void +InitializeTimeouts(void) +{ + int i; + + /* Initialize, or re-initialize, all local state */ + disable_alarm(); + + num_active_timeouts = 0; + + for (i = 0; i < MAX_TIMEOUTS; i++) + { + all_timeouts[i].index = i; + all_timeouts[i].active = false; + all_timeouts[i].indicator = false; + all_timeouts[i].timeout_handler = NULL; + all_timeouts[i].start_time = 0; + all_timeouts[i].fin_time = 0; + } + + all_timeouts_initialized = true; + + /* Now establish the signal handler */ + pqsignal(SIGALRM, handle_sig_alarm); +} + +/* + * Register a timeout reason + * + * For predefined timeouts, this just registers the callback function. + * + * For user-defined timeouts, pass id == USER_TIMEOUT; we then allocate and + * return a timeout ID. + */ +TimeoutId +RegisterTimeout(TimeoutId id, timeout_handler_proc handler) +{ + Assert(all_timeouts_initialized); + + /* There's no need to disable the signal handler here. */ + + if (id >= USER_TIMEOUT) + { + /* Allocate a user-defined timeout reason */ + for (id = USER_TIMEOUT; id < MAX_TIMEOUTS; id++) + if (all_timeouts[id].timeout_handler == NULL) + break; + if (id >= MAX_TIMEOUTS) + ereport(FATAL, + (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), + errmsg("cannot add more timeout reasons"))); + } + + Assert(all_timeouts[id].timeout_handler == NULL); + + all_timeouts[id].timeout_handler = handler; + + return id; +} + +/* + * Reschedule any pending SIGALRM interrupt. + * + * This can be used during error recovery in case query cancel resulted in loss + * of a SIGALRM event (due to longjmp'ing out of handle_sig_alarm before it + * could do anything). But note it's not necessary if any of the public + * enable_ or disable_timeout functions are called in the same area, since + * those all do schedule_alarm() internally if needed. + */ +void +reschedule_timeouts(void) +{ + /* For flexibility, allow this to be called before we're initialized. */ + if (!all_timeouts_initialized) + return; + + /* Disable timeout interrupts for safety. */ + disable_alarm(); + + /* Reschedule the interrupt, if any timeouts remain active. */ + if (num_active_timeouts > 0) + schedule_alarm(GetCurrentTimestamp()); +} + +/* + * Enable the specified timeout to fire after the specified delay. + * + * Delay is given in milliseconds. + */ +void +enable_timeout_after(TimeoutId id, int delay_ms) +{ + TimestampTz now; + TimestampTz fin_time; + + /* Disable timeout interrupts for safety. */ + disable_alarm(); + + /* Queue the timeout at the appropriate time. */ + now = GetCurrentTimestamp(); + fin_time = TimestampTzPlusMilliseconds(now, delay_ms); + enable_timeout(id, now, fin_time); + + /* Set the timer interrupt. */ + schedule_alarm(now); +} + +/* + * Enable the specified timeout to fire at the specified time. + * + * This is provided to support cases where there's a reason to calculate + * the timeout by reference to some point other than "now". If there isn't, + * use enable_timeout_after(), to avoid calling GetCurrentTimestamp() twice. + */ +void +enable_timeout_at(TimeoutId id, TimestampTz fin_time) +{ + TimestampTz now; + + /* Disable timeout interrupts for safety. */ + disable_alarm(); + + /* Queue the timeout at the appropriate time. */ + now = GetCurrentTimestamp(); + enable_timeout(id, now, fin_time); + + /* Set the timer interrupt. */ + schedule_alarm(now); +} + +/* + * Enable multiple timeouts at once. + * + * This works like calling enable_timeout_after() and/or enable_timeout_at() + * multiple times. Use this to reduce the number of GetCurrentTimestamp() + * and setitimer() calls needed to establish multiple timeouts. + */ +void +enable_timeouts(const EnableTimeoutParams *timeouts, int count) +{ + TimestampTz now; + int i; + + /* Disable timeout interrupts for safety. */ + disable_alarm(); + + /* Queue the timeout(s) at the appropriate times. */ + now = GetCurrentTimestamp(); + + for (i = 0; i < count; i++) + { + TimeoutId id = timeouts[i].id; + TimestampTz fin_time; + + switch (timeouts[i].type) + { + case TMPARAM_AFTER: + fin_time = TimestampTzPlusMilliseconds(now, + timeouts[i].delay_ms); + enable_timeout(id, now, fin_time); + break; + + case TMPARAM_AT: + enable_timeout(id, now, timeouts[i].fin_time); + break; + + default: + elog(ERROR, "unrecognized timeout type %d", + (int) timeouts[i].type); + break; + } + } + + /* Set the timer interrupt. */ + schedule_alarm(now); +} + +/* + * Cancel the specified timeout. + * + * The timeout's I've-been-fired indicator is reset, + * unless keep_indicator is true. + * + * When a timeout is canceled, any other active timeout remains in force. + * It's not an error to disable a timeout that is not enabled. + */ +void +disable_timeout(TimeoutId id, bool keep_indicator) +{ + /* Assert request is sane */ + Assert(all_timeouts_initialized); + Assert(all_timeouts[id].timeout_handler != NULL); + + /* Disable timeout interrupts for safety. */ + disable_alarm(); + + /* Find the timeout and remove it from the active list. */ + if (all_timeouts[id].active) + remove_timeout_index(find_active_timeout(id)); + + /* Mark it inactive, whether it was active or not. */ + if (!keep_indicator) + all_timeouts[id].indicator = false; + + /* Reschedule the interrupt, if any timeouts remain active. */ + if (num_active_timeouts > 0) + schedule_alarm(GetCurrentTimestamp()); +} + +/* + * Cancel multiple timeouts at once. + * + * The timeouts' I've-been-fired indicators are reset, + * unless timeouts[i].keep_indicator is true. + * + * This works like calling disable_timeout() multiple times. + * Use this to reduce the number of GetCurrentTimestamp() + * and setitimer() calls needed to cancel multiple timeouts. + */ +void +disable_timeouts(const DisableTimeoutParams *timeouts, int count) +{ + int i; + + Assert(all_timeouts_initialized); + + /* Disable timeout interrupts for safety. */ + disable_alarm(); + + /* Cancel the timeout(s). */ + for (i = 0; i < count; i++) + { + TimeoutId id = timeouts[i].id; + + Assert(all_timeouts[id].timeout_handler != NULL); + + if (all_timeouts[id].active) + remove_timeout_index(find_active_timeout(id)); + + if (!timeouts[i].keep_indicator) + all_timeouts[id].indicator = false; + } + + /* Reschedule the interrupt, if any timeouts remain active. */ + if (num_active_timeouts > 0) + schedule_alarm(GetCurrentTimestamp()); +} + +/* + * Disable the signal handler, remove all timeouts from the active list, + * and optionally reset their timeout indicators. + */ +void +disable_all_timeouts(bool keep_indicators) +{ + int i; + + disable_alarm(); + + /* + * We used to disable the timer interrupt here, but in common usage + * patterns it's cheaper to leave it enabled; that may save us from having + * to enable it again shortly. See comments in schedule_alarm(). + */ + + num_active_timeouts = 0; + + for (i = 0; i < MAX_TIMEOUTS; i++) + { + all_timeouts[i].active = false; + if (!keep_indicators) + all_timeouts[i].indicator = false; + } +} + +/* + * Return true if the timeout is active (enabled and not yet fired) + * + * This is, of course, subject to race conditions, as the timeout could fire + * immediately after we look. + */ +bool +get_timeout_active(TimeoutId id) +{ + return all_timeouts[id].active; +} + +/* + * Return the timeout's I've-been-fired indicator + * + * If reset_indicator is true, reset the indicator when returning true. + * To avoid missing timeouts due to race conditions, we are careful not to + * reset the indicator when returning false. + */ +bool +get_timeout_indicator(TimeoutId id, bool reset_indicator) +{ + if (all_timeouts[id].indicator) + { + if (reset_indicator) + all_timeouts[id].indicator = false; + return true; + } + return false; +} + +/* + * Return the time when the timeout was most recently activated + * + * Note: will return 0 if timeout has never been activated in this process. + * However, we do *not* reset the start_time when a timeout occurs, so as + * not to create a race condition if SIGALRM fires just as some code is + * about to fetch the value. + */ +TimestampTz +get_timeout_start_time(TimeoutId id) +{ + return all_timeouts[id].start_time; +} + +/* + * Return the time when the timeout is, or most recently was, due to fire + * + * Note: will return 0 if timeout has never been activated in this process. + * However, we do *not* reset the fin_time when a timeout occurs, so as + * not to create a race condition if SIGALRM fires just as some code is + * about to fetch the value. + */ +TimestampTz +get_timeout_finish_time(TimeoutId id) +{ + return all_timeouts[id].fin_time; +} |