├── 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 |
--------------------------------------------------------------------------------