├── pg_timeout.control ├── Makefile ├── pg_timeout--1.0.sql ├── META.json ├── README.md └── pg_timeout.c /pg_timeout.control: -------------------------------------------------------------------------------- 1 | # pg_timeout extension 2 | comment = 'pg_timeout extension' 3 | default_version = '1.0' 4 | module_pathname = '$libdir/pg_timeout' 5 | relocatable = true 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # pg_timeout Makefile 2 | 3 | MODULES = pg_timeout 4 | 5 | EXTENSION = pg_timeout 6 | DATA = pg_timeout--1.0.sql 7 | PGFILEDESC = "pg_timeout - backgroud worker to enable session timeout" 8 | 9 | PG_CONFIG = pg_config 10 | PGXS := $(shell $(PG_CONFIG) --pgxs) 11 | include $(PGXS) 12 | -------------------------------------------------------------------------------- /pg_timeout--1.0.sql: -------------------------------------------------------------------------------- 1 | /* pg_timeout--1.0.sql */ 2 | 3 | -- complain if script is sourced in psql, rather than via CREATE EXTENSION 4 | \echo Use "CREATE EXTENSION pg_timeout" to load this file. \quit 5 | 6 | CREATE FUNCTION pg_timeout_main() 7 | RETURNS pg_catalog.int4 STRICT 8 | AS 'MODULE_PATHNAME' 9 | LANGUAGE C; 10 | -------------------------------------------------------------------------------- /META.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pg_timeout", 3 | "abstract": "timeout for idle database session", 4 | "version": "1.0.0", 5 | "maintainer": [ 6 | "Pierre Forstmann" 7 | ], 8 | "license": { 9 | "PostgreSQL": "http://www.postgresql.org/about/licence" 10 | }, 11 | "prereqs": { 12 | "runtime": { 13 | "requires": { 14 | "PostgreSQL": "9.5.21" 15 | }, 16 | "recommends": { 17 | "PostgreSQL": "9.5.21" 18 | } 19 | } 20 | }, 21 | "provides": { 22 | "pg_queryid": { 23 | "abstract": "timeout for database session", 24 | "file": "pg_timeout.c", 25 | "docfile": "README.md", 26 | "version": "1.0.0" 27 | } 28 | }, 29 | "resources": { 30 | "bugtracker": { 31 | "web": "http://github.com/pierreforstmann/pg_timeout/issues" 32 | }, 33 | "repository": { 34 | "url": "https://github.com/pierreforstmann/pg_timeout.git", 35 | "web": "https://github.com/pierreforstmann/pg_timeout", 36 | "type": "git" 37 | } 38 | }, 39 | "generated_by": "Pierre Forstmann", 40 | "meta-spec": { 41 | "version": "1.0.0", 42 | "url": "http://pgxn.org/meta/spec.txt" 43 | }, 44 | "tags": [ 45 | "timeout", 46 | "idle", 47 | "session" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pg_timeout 2 | 3 | PostgreSQL extension to manage database idle session timeout. 4 | 5 | This extension is no more maintained because `idle_session_timeout` parameter can be used in current PostgreSQL versions. 6 | 7 | # Installation 8 | ## Compiling 9 | 10 | This module can be built using the standard PGXS infrastructure. For this to work, the pg_config program must be available in your $PATH: 11 | 12 | `git clone https://github.com/pierreforstmann/pg_timeout.git`
13 | `cd pg_timeout`
14 | `make`
15 | `make install`
16 | 17 | This extension has been validated with PostgresSQL 9.5, 9.6, 10, 11, 12, 13, 14, 15 and 16. 18 | 19 | ## PostgreSQL setup 20 | 21 | Extension can be loaded at server level with `shared_preload_libraries` parameter:
22 | `shared_preload_libraries = 'pg_timeout'` 23 | 24 | # Usage 25 | 26 | pg_timeout has 2 specific GUC:
27 | - `pg_timeout.naptime`: number of seconds for the dedicated backgroud worker to sleep between idle session checks (default value is 10 seconds)
28 | - `pg_timeout.idle_session_timeout`: database session idle timeout in seconds (default value is 60 seconds)
29 | 30 | Note that pg_timeout only takes care of database session with idle status (idle in transaction is not taken into account). 31 | 32 | ## Example 33 | 34 | Add in postgresql.conf:
35 | `shared_preload_libraries = 'pg_timeout'`
36 | `pg_timeout.naptime=30`
37 | `pg_timeout.idle_session_timeout=30`
38 | 39 | Any database session which is idle for more than 30 seconds is killed. In database instance log you get messages similar to:
40 | `LOG: pg_timeout_worker: idle session PID=26546 user=pierre database=pierre application=psql hostname=NULL`
41 | `LOG: pg_timeout_worker: idle session(s) since 30 seconds terminated`
42 | `FATAL: terminating connection due to administrator command` 43 | 44 | If the database session was started by psql, you get: 45 | 46 | `FATAL: terminating connection due to administrator command`
47 | `server closed the connection unexpectedly`
48 | `This probably means the server terminated abnormally`
49 | `before or while processing the request.`
50 | `The connection to the server was lost. Attempting reset: Succeeded.`
51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /pg_timeout.c: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------------------------- 2 | * 3 | * pg_timeout.c 4 | * 5 | * Background code to handle session timeout. 6 | * 7 | * This code is reusing worker_spi.c code from PostgresSQL code. 8 | * 9 | * Copyright 2020 Pierre Forstmann 10 | * ------------------------------------------------------------------------- 11 | */ 12 | #include "postgres.h" 13 | 14 | /* These are always necessary for a bgworker */ 15 | #include "miscadmin.h" 16 | #include "postmaster/bgworker.h" 17 | #include "storage/ipc.h" 18 | #include "storage/latch.h" 19 | #include "storage/lwlock.h" 20 | #include "storage/proc.h" 21 | #include "storage/shmem.h" 22 | 23 | /* these headers are used by this particular worker's code */ 24 | #include "access/xact.h" 25 | #include "executor/spi.h" 26 | #include "fmgr.h" 27 | #include "lib/stringinfo.h" 28 | #include "pgstat.h" 29 | #include "utils/builtins.h" 30 | #include "utils/snapmgr.h" 31 | #include "tcop/utility.h" 32 | 33 | PG_MODULE_MAGIC; 34 | 35 | PG_FUNCTION_INFO_V1(pg_timeout_main); 36 | 37 | void _PG_init(void); 38 | 39 | /* 40 | * flags set by signal handlers 41 | * */ 42 | static volatile sig_atomic_t got_sighup = false; 43 | static volatile sig_atomic_t got_sigterm = false; 44 | 45 | /* GUC variables */ 46 | 47 | /* 48 | * parameter default value in seconds set by _PG_init 49 | */ 50 | static int pg_timeout_idle_session_timeout = 0; 51 | static int pg_timeout_naptime = 0; 52 | 53 | #define LOG_MESSAGE "%s: idle session PID=%d user=%s database=%s application=%s hostname=%s" 54 | static char *null_value="NULL"; 55 | /* 56 | * Signal handler for SIGTERM 57 | * Set a flag to let the main loop to terminate, and set our latch to wake 58 | * it up. 59 | */ 60 | static void 61 | pg_timeout_sigterm(SIGNAL_ARGS) 62 | { 63 | int save_errno = errno; 64 | 65 | got_sigterm = true; 66 | SetLatch(MyLatch); 67 | 68 | errno = save_errno; 69 | } 70 | 71 | /* 72 | * Signal handler for SIGHUP 73 | * Set a flag to tell the main loop to reread the config file, and set 74 | * our latch to wake it up. 75 | */ 76 | static void 77 | pg_timeout_sighup(SIGNAL_ARGS) 78 | { 79 | int save_errno = errno; 80 | 81 | got_sighup = true; 82 | SetLatch(MyLatch); 83 | 84 | errno = save_errno; 85 | } 86 | 87 | Datum 88 | pg_timeout_main(PG_FUNCTION_ARGS) 89 | { 90 | StringInfoData buf_select; 91 | StringInfoData buf_kill; 92 | 93 | char *usename_val; 94 | char *datname_val; 95 | char *application_name_val; 96 | char *client_hostname_val; 97 | 98 | /* Establish signal handlers before unblocking signals. */ 99 | pqsignal(SIGHUP, pg_timeout_sighup); 100 | pqsignal(SIGTERM, pg_timeout_sigterm); 101 | 102 | /* We're now ready to receive signals */ 103 | BackgroundWorkerUnblockSignals(); 104 | 105 | /* Connect to our database */ 106 | #if PG_VERSION_NUM >=110000 107 | BackgroundWorkerInitializeConnection("postgres", NULL, 0); 108 | #else 109 | BackgroundWorkerInitializeConnection("postgres", NULL); 110 | #endif 111 | elog(LOG, "%s initialized", MyBgworkerEntry->bgw_name); 112 | 113 | /* 114 | */ 115 | 116 | /* In PG 9.5 and 9.6 only client backend are taken into account. 117 | * In PG 10 and above, background workers are also taken into account 118 | * but with state and stage_change set to null: 119 | * no need to filter on backend_type. 120 | */ 121 | initStringInfo(&buf_select); 122 | appendStringInfo(&buf_select, 123 | "SELECT pid, usename, datname, application_name, client_hostname " 124 | "FROM pg_stat_activity " 125 | "WHERE pid <> pg_backend_pid() " 126 | "AND state = 'idle' " 127 | "AND state_change < current_timestamp - INTERVAL '%d' SECOND", 128 | pg_timeout_idle_session_timeout 129 | ); 130 | initStringInfo(&buf_kill); 131 | appendStringInfo(&buf_kill, 132 | "SELECT pg_terminate_backend(pid) " 133 | "FROM pg_stat_activity " 134 | "WHERE pid <> pg_backend_pid() " 135 | "AND state = 'idle' " 136 | "AND state_change < current_timestamp - INTERVAL '%d' SECOND", 137 | pg_timeout_idle_session_timeout 138 | ); 139 | /* 140 | * Main loop: do this until the SIGTERM handler tells us to terminate 141 | */ 142 | 143 | while (!got_sigterm) 144 | { 145 | int ret; 146 | int rc; 147 | int nr; 148 | int i; 149 | 150 | /* 151 | * Background workers mustn't call usleep() or any direct equivalent: 152 | * instead, they may wait on their process latch, which sleeps as 153 | * necessary, but is awakened if postmaster dies. That way the 154 | * background process goes away immediately in an emergency. 155 | */ 156 | #if PG_VERSION_NUM >= 100000 157 | rc = WaitLatch(MyLatch, 158 | WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH, 159 | pg_timeout_naptime * 1000L, 160 | PG_WAIT_EXTENSION); 161 | #else 162 | rc = WaitLatch(MyLatch, 163 | WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH, 164 | pg_timeout_naptime * 1000L); 165 | #endif 166 | ResetLatch(MyLatch); 167 | 168 | /* emergency bailout if postmaster has died */ 169 | if (rc & WL_POSTMASTER_DEATH) 170 | proc_exit(1); 171 | 172 | CHECK_FOR_INTERRUPTS(); 173 | 174 | /* 175 | * In case of a SIGHUP, just reload the configuration. 176 | */ 177 | if (got_sighup) 178 | { 179 | got_sighup = false; 180 | ProcessConfigFile(PGC_SIGHUP); 181 | } 182 | 183 | /* 184 | * Start a transaction on which we can run queries. Note that each 185 | * StartTransactionCommand() call should be preceded by a 186 | * SetCurrentStatementStartTimestamp() call, which sets both the time 187 | * for the statement we're about the run, and also the transaction 188 | * start time. Also, each other query sent to SPI should probably be 189 | * preceded by SetCurrentStatementStartTimestamp(), so that statement 190 | * start time is always up to date. 191 | * 192 | * The SPI_connect() call lets us run queries through the SPI manager, 193 | * and the PushActiveSnapshot() call creates an "active" snapshot 194 | * which is necessary for queries to have MVCC data to work on. 195 | * 196 | * The pgstat_report_activity() call makes our activity visible 197 | * through the pgstat views. 198 | */ 199 | SetCurrentStatementStartTimestamp(); 200 | StartTransactionCommand(); 201 | SPI_connect(); 202 | PushActiveSnapshot(GetTransactionSnapshot()); 203 | pgstat_report_activity(STATE_RUNNING, buf_select.data); 204 | 205 | /* We can now execute queries via SPI */ 206 | ret = SPI_execute(buf_select.data, false, 0); 207 | 208 | if (ret != SPI_OK_SELECT) 209 | elog(FATAL, "cannot select from pg_stat_activity: error code %d", 210 | ret); 211 | nr = SPI_processed; 212 | 213 | i = 0; 214 | for (i = 0; i < nr; i++) 215 | { 216 | bool isnull; 217 | int32 pid_val; 218 | 219 | pid_val = DatumGetInt32(SPI_getbinval(SPI_tuptable->vals[i], 220 | SPI_tuptable->tupdesc, 221 | 1, &isnull)); 222 | if (!isnull) 223 | { 224 | usename_val = SPI_getvalue(SPI_tuptable->vals[i], 225 | SPI_tuptable->tupdesc, 226 | 2); 227 | datname_val = SPI_getvalue(SPI_tuptable->vals[i], 228 | SPI_tuptable->tupdesc, 229 | 3); 230 | application_name_val = SPI_getvalue(SPI_tuptable->vals[i], 231 | SPI_tuptable->tupdesc, 232 | 4); 233 | client_hostname_val = SPI_getvalue(SPI_tuptable->vals[i], 234 | SPI_tuptable->tupdesc, 235 | 5); 236 | /* 237 | * client_hostname_val is NULL pointer because column client_hostname is null. 238 | */ 239 | if (usename_val == NULL) 240 | usename_val = null_value; 241 | if (datname_val == NULL) 242 | datname_val = null_value; 243 | if (application_name_val == NULL) 244 | client_hostname_val = null_value; 245 | if (client_hostname_val == NULL) 246 | client_hostname_val = null_value; 247 | 248 | elog(LOG, LOG_MESSAGE, 249 | MyBgworkerEntry->bgw_name, pid_val, usename_val, 250 | datname_val, application_name_val, 251 | client_hostname_val); 252 | } 253 | else elog(WARNING, "%s: pid is NULL", 254 | MyBgworkerEntry->bgw_name); 255 | } 256 | 257 | if (nr > 0) 258 | { 259 | ret = SPI_execute(buf_kill.data, false, 0); 260 | if (ret != SPI_OK_SELECT) 261 | elog(FATAL, "cannot select pg_terminate_backend: error code %d", 262 | ret); 263 | 264 | elog(LOG, "%s: idle session(s) since %d seconds terminated", 265 | MyBgworkerEntry->bgw_name, 266 | pg_timeout_idle_session_timeout); 267 | } 268 | 269 | /* 270 | * And finish our transaction. 271 | */ 272 | SPI_finish(); 273 | PopActiveSnapshot(); 274 | CommitTransactionCommand(); 275 | pgstat_report_stat(false); 276 | pgstat_report_activity(STATE_IDLE, NULL); 277 | } 278 | 279 | proc_exit(1); 280 | } 281 | 282 | /* 283 | * Entrypoint of this module. 284 | * 285 | */ 286 | void 287 | _PG_init(void) 288 | { 289 | BackgroundWorker worker; 290 | 291 | /* get the configuration */ 292 | DefineCustomIntVariable("pg_timeout.naptime", 293 | "Duration between each check (in seconds).", 294 | NULL, 295 | &pg_timeout_naptime, 296 | 10, 297 | 1, 298 | INT_MAX, 299 | PGC_SIGHUP, 300 | 0, 301 | NULL, 302 | NULL, 303 | NULL); 304 | 305 | if (!process_shared_preload_libraries_in_progress) 306 | return; 307 | 308 | DefineCustomIntVariable("pg_timeout.idle_session_timeout", 309 | "Maximum idle session time.", 310 | NULL, 311 | &pg_timeout_idle_session_timeout, 312 | 60, 313 | 1, 314 | INT_MAX, 315 | PGC_SIGHUP, 316 | 0, 317 | NULL, 318 | NULL, 319 | NULL); 320 | 321 | /* set up common data for all our workers */ 322 | memset(&worker, 0, sizeof(worker)); 323 | worker.bgw_flags = BGWORKER_SHMEM_ACCESS | 324 | BGWORKER_BACKEND_DATABASE_CONNECTION; 325 | worker.bgw_start_time = BgWorkerStart_RecoveryFinished; 326 | worker.bgw_restart_time = pg_timeout_naptime; 327 | sprintf(worker.bgw_library_name, "pg_timeout"); 328 | sprintf(worker.bgw_function_name, "pg_timeout_main"); 329 | worker.bgw_notify_pid = 0; 330 | 331 | snprintf(worker.bgw_name, BGW_MAXLEN, "pg_timeout_worker"); 332 | #if PG_VERSION_NUM >= 110000 333 | snprintf(worker.bgw_type, BGW_MAXLEN, "pg_timeout"); 334 | #endif 335 | worker.bgw_main_arg = 0; 336 | 337 | RegisterBackgroundWorker(&worker); 338 | 339 | elog(LOG, "%s started with pg_timeout.naptime=%d seconds", 340 | worker.bgw_name, 341 | pg_timeout_naptime); 342 | 343 | elog(LOG, "%s started with pg_timeout.idle_session_timeout=%d seconds", 344 | worker.bgw_name, 345 | pg_timeout_idle_session_timeout); 346 | } 347 | 348 | --------------------------------------------------------------------------------