diff options
Diffstat (limited to 'src/backend/utils/misc/timeout.c')
-rw-r--r-- | src/backend/utils/misc/timeout.c | 685 |
1 files changed, 685 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..f1c9518 --- /dev/null +++ b/src/backend/utils/misc/timeout.c @@ -0,0 +1,685 @@ +/*------------------------------------------------------------------------- + * + * timeout.c + * Routines to multiplex SIGALRM interrupts for multiple timeout reasons. + * + * Portions Copyright (c) 1996-2020, 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. + * We leave this "false" when we're not expecting interrupts, just in case. + * + * Note that we don't bother to reset any pending timer interrupt when we + * disable the signal handler; it's not really worth the cycles to do so, + * since the probability of the interrupt actually occurring while we have + * it disabled is low. See comments in schedule_alarm() about that. + */ +static volatile sig_atomic_t alarm_enabled = false; + +#define disable_alarm() (alarm_enabled = false) +#define enable_alarm() (alarm_enabled = true) + + +/***************************************************************************** + * 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. + */ +static void +schedule_alarm(TimestampTz now) +{ + if (num_active_timeouts > 0) + { + struct itimerval timeval; + long secs; + int usecs; + + MemSet(&timeval, 0, sizeof(struct itimerval)); + + /* Get the time remaining till the nearest pending timeout */ + TimestampDifference(now, active_timeouts[0]->fin_time, + &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 reset 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(); + + /* Set the alarm timer */ + if (setitimer(ITIMER_REAL, &timeval, NULL) != 0) + 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); + + /* + * 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 SIGALRM and remove all timeouts from the active list, + * and optionally reset their timeout indicators. + */ +void +disable_all_timeouts(bool keep_indicators) +{ + int i; + + disable_alarm(); + + /* + * Only bother to reset the timer if we think it's active. We could just + * let the interrupt happen anyway, but it's probably a bit cheaper to do + * setitimer() than to let the useless interrupt happen. + */ + if (num_active_timeouts > 0) + { + struct itimerval timeval; + + MemSet(&timeval, 0, sizeof(struct itimerval)); + if (setitimer(ITIMER_REAL, &timeval, NULL) != 0) + elog(FATAL, "could not disable SIGALRM timer: %m"); + } + + 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; +} |