Commit d43837d0 authored by Tom Lane's avatar Tom Lane

Add lock_timeout configuration parameter.

This GUC allows limiting the time spent waiting to acquire any one
heavyweight lock.

In support of this, improve the recently-added timeout infrastructure
to permit efficiently enabling or disabling multiple timeouts at once.
That reduces the performance hit from turning on lock_timeout, though
it's still not zero.

Zoltán Böszörményi, reviewed by Tom Lane,
Stephen Frost, and Hari Babu
parent d2bef5f7
......@@ -5077,7 +5077,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</indexterm>
<listitem>
<para>
Abort any statement that takes over the specified number of
Abort any statement that takes more than the specified number of
milliseconds, starting from the time the command arrives at the server
from the client. If <varname>log_min_error_statement</> is set to
<literal>ERROR</> or lower, the statement that timed out will also be
......@@ -5086,8 +5086,42 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
<para>
Setting <varname>statement_timeout</> in
<filename>postgresql.conf</> is not recommended because it
affects all sessions.
<filename>postgresql.conf</> is not recommended because it would
affect all sessions.
</para>
</listitem>
</varlistentry>
<varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
<term><varname>lock_timeout</varname> (<type>integer</type>)</term>
<indexterm>
<primary><varname>lock_timeout</> configuration parameter</primary>
</indexterm>
<listitem>
<para>
Abort any statement that waits longer than the specified number of
milliseconds while attempting to acquire a lock on a table, index,
row, or other database object. The time limit applies separately to
each lock acquisition attempt. The limit applies both to explicit
locking requests (such as <command>LOCK TABLE</>, or <command>SELECT
FOR UPDATE</> without <literal>NOWAIT</>) and to implicitly-acquired
locks. If <varname>log_min_error_statement</> is set to
<literal>ERROR</> or lower, the statement that timed out will be
logged. A value of zero (the default) turns this off.
</para>
<para>
Unlike <varname>statement_timeout</>, this timeout can only occur
while waiting for locks. Note that if <varname>statement_timeout</>
is nonzero, it is rather pointless to set <varname>lock_timeout</> to
the same or larger value, since the statement timeout would always
trigger first.
</para>
<para>
Setting <varname>lock_timeout</> in
<filename>postgresql.conf</> is not recommended because it would
affect all sessions.
</para>
</listitem>
</varlistentry>
......
......@@ -547,10 +547,11 @@ AutoVacLauncherMain(int argc, char *argv[])
SetConfigOption("zero_damaged_pages", "false", PGC_SUSET, PGC_S_OVERRIDE);
/*
* Force statement_timeout to zero to avoid a timeout setting from
* preventing regular maintenance from being executed.
* Force statement_timeout and lock_timeout to zero to avoid letting these
* settings prevent regular maintenance from being executed.
*/
SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
/*
* Force default_transaction_isolation to READ COMMITTED. We don't want
......@@ -1573,10 +1574,11 @@ AutoVacWorkerMain(int argc, char *argv[])
SetConfigOption("zero_damaged_pages", "false", PGC_SUSET, PGC_S_OVERRIDE);
/*
* Force statement_timeout to zero to avoid a timeout setting from
* preventing regular maintenance from being executed.
* Force statement_timeout and lock_timeout to zero to avoid letting these
* settings prevent regular maintenance from being executed.
*/
SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
/*
* Force default_transaction_isolation to READ COMMITTED. We don't want
......
......@@ -428,8 +428,15 @@ ResolveRecoveryConflictWithBufferPin(void)
* Wake up at ltime, and check for deadlocks as well if we will be
* waiting longer than deadlock_timeout
*/
enable_timeout_after(STANDBY_DEADLOCK_TIMEOUT, DeadlockTimeout);
enable_timeout_at(STANDBY_TIMEOUT, ltime);
EnableTimeoutParams timeouts[2];
timeouts[0].id = STANDBY_TIMEOUT;
timeouts[0].type = TMPARAM_AT;
timeouts[0].fin_time = ltime;
timeouts[1].id = STANDBY_DEADLOCK_TIMEOUT;
timeouts[1].type = TMPARAM_AFTER;
timeouts[1].delay_ms = DeadlockTimeout;
enable_timeouts(timeouts, 2);
}
/* Wait to be signaled by UnpinBuffer() */
......
......@@ -55,6 +55,7 @@
/* GUC variables */
int DeadlockTimeout = 1000;
int StatementTimeout = 0;
int LockTimeout = 0;
bool log_lock_waits = false;
/* Pointer to this process's PGPROC and PGXACT structs, if any */
......@@ -665,6 +666,7 @@ void
LockErrorCleanup(void)
{
LWLockId partitionLock;
DisableTimeoutParams timeouts[2];
AbortStrongLockAcquire();
......@@ -672,8 +674,19 @@ LockErrorCleanup(void)
if (lockAwaited == NULL)
return;
/* Turn off the deadlock timer, if it's still running (see ProcSleep) */
disable_timeout(DEADLOCK_TIMEOUT, false);
/*
* Turn off the deadlock and lock timeout timers, if they are still
* running (see ProcSleep). Note we must preserve the LOCK_TIMEOUT
* indicator flag, since this function is executed before
* ProcessInterrupts when responding to SIGINT; else we'd lose the
* knowledge that the SIGINT came from a lock timeout and not an external
* source.
*/
timeouts[0].id = DEADLOCK_TIMEOUT;
timeouts[0].keep_indicator = false;
timeouts[1].id = LOCK_TIMEOUT;
timeouts[1].keep_indicator = true;
disable_timeouts(timeouts, 2);
/* Unlink myself from the wait queue, if on it (might not be anymore!) */
partitionLock = LockHashPartitionLock(lockAwaited->hashcode);
......@@ -1072,8 +1085,24 @@ ProcSleep(LOCALLOCK *locallock, LockMethod lockMethodTable)
*
* By delaying the check until we've waited for a bit, we can avoid
* running the rather expensive deadlock-check code in most cases.
*
* If LockTimeout is set, also enable the timeout for that. We can save a
* few cycles by enabling both timeout sources in one call.
*/
enable_timeout_after(DEADLOCK_TIMEOUT, DeadlockTimeout);
if (LockTimeout > 0)
{
EnableTimeoutParams timeouts[2];
timeouts[0].id = DEADLOCK_TIMEOUT;
timeouts[0].type = TMPARAM_AFTER;
timeouts[0].delay_ms = DeadlockTimeout;
timeouts[1].id = LOCK_TIMEOUT;
timeouts[1].type = TMPARAM_AFTER;
timeouts[1].delay_ms = LockTimeout;
enable_timeouts(timeouts, 2);
}
else
enable_timeout_after(DEADLOCK_TIMEOUT, DeadlockTimeout);
/*
* If someone wakes us between LWLockRelease and PGSemaphoreLock,
......@@ -1240,9 +1269,20 @@ ProcSleep(LOCALLOCK *locallock, LockMethod lockMethodTable)
} while (myWaitStatus == STATUS_WAITING);
/*
* Disable the timer, if it's still running
* Disable the timers, if they are still running
*/
disable_timeout(DEADLOCK_TIMEOUT, false);
if (LockTimeout > 0)
{
DisableTimeoutParams timeouts[2];
timeouts[0].id = DEADLOCK_TIMEOUT;
timeouts[0].keep_indicator = false;
timeouts[1].id = LOCK_TIMEOUT;
timeouts[1].keep_indicator = false;
disable_timeouts(timeouts, 2);
}
else
disable_timeout(DEADLOCK_TIMEOUT, false);
/*
* Re-acquire the lock table's partition lock. We have to do this to hold
......
......@@ -2883,7 +2883,22 @@ ProcessInterrupts(void)
(errcode(ERRCODE_QUERY_CANCELED),
errmsg("canceling authentication due to timeout")));
}
if (get_timeout_indicator(STATEMENT_TIMEOUT))
/*
* If LOCK_TIMEOUT and STATEMENT_TIMEOUT indicators are both set, we
* prefer to report the former; but be sure to clear both.
*/
if (get_timeout_indicator(LOCK_TIMEOUT, true))
{
ImmediateInterruptOK = false; /* not idle anymore */
(void) get_timeout_indicator(STATEMENT_TIMEOUT, true);
DisableNotifyInterrupt();
DisableCatchupInterrupt();
ereport(ERROR,
(errcode(ERRCODE_QUERY_CANCELED),
errmsg("canceling statement due to lock timeout")));
}
if (get_timeout_indicator(STATEMENT_TIMEOUT, true))
{
ImmediateInterruptOK = false; /* not idle anymore */
DisableNotifyInterrupt();
......
......@@ -67,6 +67,7 @@ static void CheckMyDatabase(const char *name, bool am_superuser);
static void InitCommunication(void);
static void ShutdownPostgres(int code, Datum arg);
static void StatementTimeoutHandler(void);
static void LockTimeoutHandler(void);
static bool ThereIsAtLeastOneRole(void);
static void process_startup_options(Port *port, bool am_superuser);
static void process_settings(Oid databaseid, Oid roleid);
......@@ -535,6 +536,7 @@ InitPostgres(const char *in_dbname, Oid dboid, const char *username,
{
RegisterTimeout(DEADLOCK_TIMEOUT, CheckDeadLock);
RegisterTimeout(STATEMENT_TIMEOUT, StatementTimeoutHandler);
RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
}
/*
......@@ -1052,6 +1054,22 @@ StatementTimeoutHandler(void)
kill(MyProcPid, SIGINT);
}
/*
* LOCK_TIMEOUT handler: trigger a query-cancel interrupt.
*
* This is identical to StatementTimeoutHandler, but since it's so short,
* we might as well keep the two functions separate for clarity.
*/
static void
LockTimeoutHandler(void)
{
#ifdef HAVE_SETSID
/* try to signal whole process group */
kill(-MyProcPid, SIGINT);
#endif
kill(MyProcPid, SIGINT);
}
/*
* Returns true if at least one role is defined in this database cluster.
......
......@@ -1861,6 +1861,17 @@ static struct config_int ConfigureNamesInt[] =
NULL, NULL, NULL
},
{
{"lock_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
gettext_noop("Sets the maximum allowed duration of any wait for a lock."),
gettext_noop("A value of 0 turns off the timeout."),
GUC_UNIT_MS
},
&LockTimeout,
0, 0, INT_MAX,
NULL, NULL, NULL
},
{
{"vacuum_freeze_min_age", PGC_USERSET, CLIENT_CONN_STATEMENT,
gettext_noop("Minimum age at which VACUUM should freeze a table row."),
......
......@@ -489,6 +489,7 @@
#default_transaction_deferrable = off
#session_replication_role = 'origin'
#statement_timeout = 0 # in milliseconds, 0 is disabled
#lock_timeout = 0 # in milliseconds, 0 is disabled
#vacuum_freeze_min_age = 50000000
#vacuum_freeze_table_age = 150000000
#bytea_output = 'hex' # hex, escape
......
This diff is collapsed.
......@@ -2592,9 +2592,12 @@ _tocEntryIsACL(TocEntry *te)
static void
_doSetFixedOutputState(ArchiveHandle *AH)
{
/* Disable statement_timeout in archive for pg_restore/psql */
/* Disable statement_timeout since restore is probably slow */
ahprintf(AH, "SET statement_timeout = 0;\n");
/* Likewise for lock_timeout */
ahprintf(AH, "SET lock_timeout = 0;\n");
/* Select the correct character set encoding */
ahprintf(AH, "SET client_encoding = '%s';\n",
pg_encoding_to_char(AH->public.encoding));
......
......@@ -957,6 +957,8 @@ setup_connection(Archive *AH, const char *dumpencoding, char *use_role)
*/
if (AH->remoteVersion >= 70300)
ExecuteSqlStatement(AH, "SET statement_timeout = 0");
if (AH->remoteVersion >= 90300)
ExecuteSqlStatement(AH, "SET lock_timeout = 0");
/*
* Quote all identifiers, if requested.
......
......@@ -168,8 +168,8 @@ typedef struct PGXACT
uint8 vacuumFlags; /* vacuum-related flags, see above */
bool overflowed;
bool delayChkpt; /* true if this proc delays checkpoint start */
/* previously called InCommit */
bool delayChkpt; /* true if this proc delays checkpoint start;
* previously called InCommit */
uint8 nxids;
} PGXACT;
......@@ -222,6 +222,7 @@ extern PGPROC *PreparedXactProcs;
/* configurable options */
extern int DeadlockTimeout;
extern int StatementTimeout;
extern int LockTimeout;
extern bool log_lock_waits;
......
......@@ -25,6 +25,7 @@ typedef enum TimeoutId
/* Predefined timeout reasons */
STARTUP_PACKET_TIMEOUT,
DEADLOCK_TIMEOUT,
LOCK_TIMEOUT,
STATEMENT_TIMEOUT,
STANDBY_DEADLOCK_TIMEOUT,
STANDBY_TIMEOUT,
......@@ -35,20 +36,48 @@ typedef enum TimeoutId
} TimeoutId;
/* callback function signature */
typedef void (*timeout_handler) (void);
typedef void (*timeout_handler_proc) (void);
/*
* Parameter structure for setting multiple timeouts at once
*/
typedef enum TimeoutType
{
TMPARAM_AFTER,
TMPARAM_AT
} TimeoutType;
typedef struct
{
TimeoutId id; /* timeout to set */
TimeoutType type; /* TMPARAM_AFTER or TMPARAM_AT */
int delay_ms; /* only used for TMPARAM_AFTER */
TimestampTz fin_time; /* only used for TMPARAM_AT */
} EnableTimeoutParams;
/*
* Parameter structure for clearing multiple timeouts at once
*/
typedef struct
{
TimeoutId id; /* timeout to clear */
bool keep_indicator; /* keep the indicator flag? */
} DisableTimeoutParams;
/* timeout setup */
extern void InitializeTimeouts(void);
extern TimeoutId RegisterTimeout(TimeoutId id, timeout_handler handler);
extern TimeoutId RegisterTimeout(TimeoutId id, timeout_handler_proc handler);
/* timeout operation */
extern void enable_timeout_after(TimeoutId id, int delay_ms);
extern void enable_timeout_at(TimeoutId id, TimestampTz fin_time);
extern void enable_timeouts(const EnableTimeoutParams *timeouts, int count);
extern void disable_timeout(TimeoutId id, bool keep_indicator);
extern void disable_timeouts(const DisableTimeoutParams *timeouts, int count);
extern void disable_all_timeouts(bool keep_indicators);
/* accessors */
extern bool get_timeout_indicator(TimeoutId id);
extern bool get_timeout_indicator(TimeoutId id, bool reset_indicator);
extern TimestampTz get_timeout_start_time(TimeoutId id);
#endif /* TIMEOUT_H */
Parsed test spec with 2 sessions
starting permutation: rdtbl sto locktbl
step rdtbl: SELECT * FROM accounts;
accountid balance
checking 600
savings 600
step sto: SET statement_timeout = 1000;
step locktbl: LOCK TABLE accounts; <waiting ...>
step locktbl: <... completed>
ERROR: canceling statement due to statement timeout
starting permutation: rdtbl lto locktbl
step rdtbl: SELECT * FROM accounts;
accountid balance
checking 600
savings 600
step lto: SET lock_timeout = 1000;
step locktbl: LOCK TABLE accounts; <waiting ...>
step locktbl: <... completed>
ERROR: canceling statement due to lock timeout
starting permutation: rdtbl lsto locktbl
step rdtbl: SELECT * FROM accounts;
accountid balance
checking 600
savings 600
step lsto: SET lock_timeout = 1000; SET statement_timeout = 2000;
step locktbl: LOCK TABLE accounts; <waiting ...>
step locktbl: <... completed>
ERROR: canceling statement due to lock timeout
starting permutation: rdtbl slto locktbl
step rdtbl: SELECT * FROM accounts;
accountid balance
checking 600
savings 600
step slto: SET lock_timeout = 2000; SET statement_timeout = 1000;
step locktbl: LOCK TABLE accounts; <waiting ...>
step locktbl: <... completed>
ERROR: canceling statement due to statement timeout
starting permutation: wrtbl sto update
step wrtbl: UPDATE accounts SET balance = balance + 100;
step sto: SET statement_timeout = 1000;
step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
step update: <... completed>
ERROR: canceling statement due to statement timeout
starting permutation: wrtbl lto update
step wrtbl: UPDATE accounts SET balance = balance + 100;
step lto: SET lock_timeout = 1000;
step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
step update: <... completed>
ERROR: canceling statement due to lock timeout
starting permutation: wrtbl lsto update
step wrtbl: UPDATE accounts SET balance = balance + 100;
step lsto: SET lock_timeout = 1000; SET statement_timeout = 2000;
step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
step update: <... completed>
ERROR: canceling statement due to lock timeout
starting permutation: wrtbl slto update
step wrtbl: UPDATE accounts SET balance = balance + 100;
step slto: SET lock_timeout = 2000; SET statement_timeout = 1000;
step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
step update: <... completed>
ERROR: canceling statement due to statement timeout
......@@ -20,3 +20,4 @@ test: delete-abort-savept
test: delete-abort-savept-2
test: aborted-keyrevoke
test: drop-index-concurrently-1
test: timeouts
# Simple tests for statement_timeout and lock_timeout features
setup
{
CREATE TABLE accounts (accountid text PRIMARY KEY, balance numeric not null);
INSERT INTO accounts VALUES ('checking', 600), ('savings', 600);
}
teardown
{
DROP TABLE accounts;
}
session "s1"
setup { BEGIN ISOLATION LEVEL READ COMMITTED; }
step "rdtbl" { SELECT * FROM accounts; }
step "wrtbl" { UPDATE accounts SET balance = balance + 100; }
teardown { ABORT; }
session "s2"
setup { BEGIN ISOLATION LEVEL READ COMMITTED; }
step "sto" { SET statement_timeout = 1000; }
step "lto" { SET lock_timeout = 1000; }
step "lsto" { SET lock_timeout = 1000; SET statement_timeout = 2000; }
step "slto" { SET lock_timeout = 2000; SET statement_timeout = 1000; }
step "locktbl" { LOCK TABLE accounts; }
step "update" { DELETE FROM accounts WHERE accountid = 'checking'; }
teardown { ABORT; }
# statement timeout, table-level lock
permutation "rdtbl" "sto" "locktbl"
# lock timeout, table-level lock
permutation "rdtbl" "lto" "locktbl"
# lock timeout expires first, table-level lock
permutation "rdtbl" "lsto" "locktbl"
# statement timeout expires first, table-level lock
permutation "rdtbl" "slto" "locktbl"
# statement timeout, row-level lock
permutation "wrtbl" "sto" "update"
# lock timeout, row-level lock
permutation "wrtbl" "lto" "update"
# lock timeout expires first, row-level lock
permutation "wrtbl" "lsto" "update"
# statement timeout expires first, row-level lock
permutation "wrtbl" "slto" "update"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment