public inbox for [email protected]  
help / color / mirror / Atom feed
transaction timeout
76+ messages / 17 participants
[nested] [flat]

* transaction timeout
@ 2001-03-17 01:10  Kevin T. Manley <[email protected]>
  0 siblings, 0 replies; 76+ messages in thread

From: Kevin T. Manley @ 2001-03-17 01:10 UTC (permalink / raw)
  To: pgsql-hackers

Is there a timeout setting I can use to abort transactions that aren't
deadlocked, but which have been blocked waiting for locks greater than some
amount of time? I didn't see anything in the docs on this and observed with
2 instances of psql that a transaction waiting on a lock seems to wait
forever.

If pgsql doesn't have such a setting, has there been any discussion about
adding it?

Regards,
Kevin Manley





^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* [PATCH v3] Fix a delay in PostgreSQL shutdown caused by logical replication
@ 2019-09-03 22:17  Alvaro Herrera <[email protected]>
  0 siblings, 0 replies; 76+ messages in thread

From: Alvaro Herrera @ 2019-09-03 22:17 UTC (permalink / raw)

Due to a race with WAL writing during shutdown, if logical walsenders were
running then PostgreSQL's shutdown could be delayed by up to
wal_sender_timeout/2 while it waits for the walsenders to shut down. The
walsenders wait for new WAL or end-of-wal which won't come until shutdown so
there's a deadlock. The walsender timeout eventually breaks the deadlock.

The issue was introduced by PostgreSQL 10 commit c6c3334364
"Prevent possibility of panics during shutdown checkpoint".

A logical walsender never enters WALSNDSTATE_STOPPING and allows the
checkpointer to continue shutdown. Instead it exits when it reads
end-of-WAL.  But if it reads the last WAL record written before shutdown
and that record doesn't generate a client network write, it can mark
itself caught up and go to sleep without checking to see if it's been
asked to shut down.

Fix by making sure the logical walsender always checks if it's been asked
to shut down before it allows the walsender main loop to go to sleep.

When this issue happens the walsender(s) can be seen to be sleeping in
WaitLatchOrSocket in WalSndLoop until woken by the keepalive timeout. The
checkpointer will be waiting in WalSndWaitStopping() for the walsenders to
enter WALSNDSTATE_STOPPING or exit, whichever happens first. The postmaster
will be waiting in ServerLoop for the checkpointer to finish the shutdown
checkpoint.
---
 src/backend/replication/walsender.c | 49 +++++++++++++----------------
 1 file changed, 21 insertions(+), 28 deletions(-)

diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 23870a25a5..1c5380fa7d 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1296,7 +1296,6 @@ WalSndWaitForWal(XLogRecPtr loc)
 	int			wakeEvents;
 	static XLogRecPtr RecentFlushPtr = InvalidXLogRecPtr;
 
-
 	/*
 	 * Fast path to avoid acquiring the spinlock in case we already know we
 	 * have enough WAL available. This is particularly interesting if we're
@@ -2814,6 +2813,7 @@ XLogSendLogical(void)
 {
 	XLogRecord *record;
 	char	   *errm;
+	XLogRecPtr	flushPtr;
 
 	/*
 	 * Don't know whether we've caught up yet. We'll set WalSndCaughtUp to
@@ -2830,11 +2830,15 @@ XLogSendLogical(void)
 	if (errm != NULL)
 		elog(ERROR, "%s", errm);
 
+	/*
+	 * Read current flush point; we'll use it to determine whether we've
+	 * caught up.  Note that logical decoding cannot be used while in
+	 * recovery, so ... XXX what?
+	 */
+	flushPtr = GetFlushRecPtr();
+
 	if (record != NULL)
 	{
-		/* XXX: Note that logical decoding cannot be used while in recovery */
-		XLogRecPtr	flushPtr = GetFlushRecPtr();
-
 		/*
 		 * Note the lack of any call to LagTrackerWrite() which is handled by
 		 * WalSndUpdateProgress which is called by output plugin through
@@ -2843,32 +2847,21 @@ XLogSendLogical(void)
 		LogicalDecodingProcessRecord(logical_decoding_ctx, logical_decoding_ctx->reader);
 
 		sentPtr = logical_decoding_ctx->reader->EndRecPtr;
-
-		/*
-		 * If we have sent a record that is at or beyond the flushed point, we
-		 * have caught up.
-		 */
-		if (sentPtr >= flushPtr)
-			WalSndCaughtUp = true;
 	}
-	else
-	{
-		/*
-		 * If the record we just wanted read is at or beyond the flushed
-		 * point, then we're caught up.
-		 */
-		if (logical_decoding_ctx->reader->EndRecPtr >= GetFlushRecPtr())
-		{
-			WalSndCaughtUp = true;
 
-			/*
-			 * Have WalSndLoop() terminate the connection in an orderly
-			 * manner, after writing out all the pending data.
-			 */
-			if (got_STOPPING)
-				got_SIGUSR2 = true;
-		}
-	}
+	/*
+	 * We might have caught up; set flag if so.
+	 */
+	if (logical_decoding_ctx->reader->EndRecPtr >= flushPtr)
+		WalSndCaughtUp = true;
+
+	/*
+	 * If we're caught up and have been requested to stop, have WalSndLoop()
+	 * terminate the connection in an orderly manner, after writing out all
+	 * the pending data.
+	 */
+	if (WalSndCaughtUp && got_STOPPING)
+		got_SIGUSR2 = true;
 
 	/* Update shared memory status */
 	{
-- 
2.17.1


--XsQoSWH+UP9D9v3l--





^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Transaction timeout
@ 2022-12-03 05:18  Andrey Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey Borodin @ 2022-12-03 05:18 UTC (permalink / raw)
  To: Nikolay Samokhvalov <[email protected]>; pgsql-hackers

Hello,

We have statement_timeout, idle_in_transaction_timeout,
idle_session_timeout and many more! But we have no
transaction_timeout. I've skimmed thread [0,1] about existing timeouts
and found no contraindications to implement transaction_timeout.

Nikolay asked me if I can prototype the feature for testing by him,
and it seems straightforward. Please find attached. If it's not known
to be a bad idea - we'll work on it.

Thanks!

Best regards, Andrey Borodin.

[0] https://www.postgresql.org/message-id/flat/763A0689-F189-459E-946F-F0EC4458980B%40hotmail.com


Attachments:

  [application/octet-stream] v1-0001-Intorduce-transaction_timeout.patch (9.8K, ../../CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ@mail.gmail.com/2-v1-0001-Intorduce-transaction_timeout.patch)
  download | inline diff:
From 91b73b077f6b5d96aeb8f386a0fa6b6a156e27d1 Mon Sep 17 00:00:00 2001
From: Andrey Borodin <[email protected]>
Date: Fri, 2 Dec 2022 21:01:29 -0800
Subject: [PATCH v1] Intorduce transaction_timeout

Just like statement_timeout, but for transaction.
---
 src/backend/postmaster/autovacuum.c  |  1 +
 src/backend/storage/lmgr/proc.c      |  1 +
 src/backend/tcop/postgres.c          | 18 ++++++++++++++++++
 src/backend/utils/errcodes.txt       |  1 +
 src/backend/utils/init/postinit.c    | 13 +++++++++++++
 src/backend/utils/misc/guc_tables.c  | 11 +++++++++++
 src/bin/pg_dump/pg_backup_archiver.c |  1 +
 src/bin/pg_dump/pg_dump.c            |  2 ++
 src/bin/pg_rewind/libpq_source.c     |  1 +
 src/include/miscadmin.h              |  1 +
 src/include/storage/proc.h           |  1 +
 src/include/utils/timeout.h          |  1 +
 12 files changed, 52 insertions(+)

diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 601834d4b4..828a28af0a 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -588,6 +588,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b1c35653fc..0170e226d0 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -61,6 +61,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 3082093d1e..45db2bdbb5 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2691,6 +2691,10 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		Assert(!get_timeout_active(TRANSACTION_TIMEOUT));
+		if (TransactionTimeout > 0)
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -2720,6 +2724,7 @@ finish_xact_command(void)
 
 	if (xact_started)
 	{
+
 		CommitTransactionCommand();
 
 #ifdef MEMORY_CONTEXT_CHECKING
@@ -3268,6 +3273,7 @@ ProcessInterrupts(void)
 	{
 		bool		lock_timeout_occurred;
 		bool		stmt_timeout_occurred;
+		bool		tx_timeout_occurred;
 
 		QueryCancelPending = false;
 
@@ -3277,6 +3283,7 @@ ProcessInterrupts(void)
 		 */
 		lock_timeout_occurred = get_timeout_indicator(LOCK_TIMEOUT, true);
 		stmt_timeout_occurred = get_timeout_indicator(STATEMENT_TIMEOUT, true);
+		tx_timeout_occurred = get_timeout_indicator(TRANSACTION_TIMEOUT, true);
 
 		/*
 		 * If both were set, we want to report whichever timeout completed
@@ -3302,6 +3309,13 @@ ProcessInterrupts(void)
 					(errcode(ERRCODE_QUERY_CANCELED),
 					 errmsg("canceling statement due to statement timeout")));
 		}
+		if (tx_timeout_occurred)
+		{
+			LockErrorCleanup();
+			ereport(ERROR,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("canceling transaction due to transaction timeout")));
+		}
 		if (IsAutoVacuumWorkerProcess())
 		{
 			LockErrorCleanup();
@@ -4460,6 +4474,10 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 62418a051a..3ae2bbda70 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index a990c833c5..56d11b5e62 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -756,6 +757,8 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT,
+						TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1360,6 +1363,16 @@ IdleInTransactionSessionTimeoutHandler(void)
 	SetLatch(MyLatch);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+#ifdef HAVE_SETSID
+	/* try to signal whole process group */
+	kill(-MyProcPid, SIGINT);
+#endif
+	kill(MyProcPid, SIGINT);
+}
+
 static void
 IdleSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1bf14eec66..ca21d2544c 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2452,6 +2452,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed in a transaction."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 0081873a72..5229fe3555 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3089,6 +3089,7 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 44e8cd4704..87be74753d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1188,6 +1188,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 160000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 011c9cce6e..1b9674140a 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 795182fa51..484384b4ed 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index aa13e1d66e..a892c72765 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -428,6 +428,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index c068986d09..c8fa0dc3d9 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
-- 
2.37.0 (Apple Git-136)



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2022-12-03 06:59  Nikolay Samokhvalov <[email protected]>
  parent: Andrey Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Nikolay Samokhvalov @ 2022-12-03 06:59 UTC (permalink / raw)
  To: Andrey Borodin <[email protected]>; +Cc: pgsql-hackers

On Fri, Dec 2, 2022 at 9:18 PM Andrey Borodin <[email protected]> wrote:

> Hello,
>
> We have statement_timeout, idle_in_transaction_timeout,
> idle_session_timeout and many more! But we have no
> transaction_timeout. I've skimmed thread [0,1] about existing timeouts
> and found no contraindications to implement transaction_timeout.
>
> Nikolay asked me if I can prototype the feature for testing by him,
> and it seems straightforward. Please find attached. If it's not known
> to be a bad idea - we'll work on it.
>

Thanks!! It was a super quick reaction to my proposal Honestly, I was
thinking about it for several years, wondering why it's still not
implemented.

The reasons to have it should be straightforward – here are a couple of
them I can see.

First one. In the OLTP context, we usually have:
- a hard timeout set in application server
- a hard timeout set in HTTP server
- users not willing to wait more than several seconds – and almost never
being ready to wait for more than 30-60s.

If Postgres allows longer transactions (it does since we cannot reliably
limit their duration now, it's always virtually unlimited), it might be
doing the work that nobody is waiting / is needing anymore, speeding
resources, affecting health, etc.

Why we cannot limit transaction duration reliably? The existing timeouts
(namely, statement_timeout + idle_session_timeout) don't protect from
having transactions consisting of a series of small statements and short
pauses between them. If such behavior happens (e.g., a long series of fast
UPDATEs in a loop). It can be dangerous, affecting general DB health (bloat
issues). This is reason number two – DBAs might want to decide to minimize
the cases of long transactions, setting transaction limits globally (and
allowing to set it locally for particular sessions or for some users in
rare cases).

Speaking of the patch – I just tested it (gitpod:
https://gitpod.io/#https://gitlab.com/NikolayS/postgres/tree/transaction_timeout);
it applies, works as expected for single-statement transactions:

postgres=# set transaction_timeout to '2s';
SET
postgres=# select pg_sleep(3);
ERROR:  canceling transaction due to transaction timeout

But it fails in the "worst" case I've described above – a series of small
statements:

postgres=# set transaction_timeout to '2s';
SET
postgres=# begin; select pg_sleep(1); select pg_sleep(1); select
pg_sleep(1); select pg_sleep(1); select pg_sleep(1); commit;
BEGIN
 pg_sleep
----------

(1 row)

 pg_sleep
----------

(1 row)

 pg_sleep
----------

(1 row)

 pg_sleep
----------

(1 row)

 pg_sleep
----------

(1 row)

COMMIT
postgres=#


^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2022-12-03 17:41  Andrey Borodin <[email protected]>
  parent: Nikolay Samokhvalov <[email protected]>
  0 siblings, 3 replies; 76+ messages in thread

From: Andrey Borodin @ 2022-12-03 17:41 UTC (permalink / raw)
  To: Nikolay Samokhvalov <[email protected]>; +Cc: pgsql-hackers

On Fri, Dec 2, 2022 at 10:59 PM Nikolay Samokhvalov
<[email protected]> wrote:
>
> But it fails in the "worst" case I've described above – a series of small statements:

Fixed. Added test for this.

Open questions:
1. Docs
2. Order of reporting if happened lock_timeout, statement_timeout, and
transaction_timeout simultaneously. Currently there's a lot of code
around this...

Thanks!

Best regards, Andrey Borodin.


Attachments:

  [application/octet-stream] v2-0001-Intorduce-transaction_timeout.patch (11.8K, ../../CAAhFRxg7p+Z=xAFAdPeuLeOvc+3DyKKJtg8PKfdE453yybfHMQ@mail.gmail.com/2-v2-0001-Intorduce-transaction_timeout.patch)
  download | inline diff:
From 462ef31450db71ee122706afda13587a31156107 Mon Sep 17 00:00:00 2001
From: Andrey Borodin <[email protected]>
Date: Fri, 2 Dec 2022 21:01:29 -0800
Subject: [PATCH v2] Intorduce transaction_timeout

Just like statement_timeout, but for transaction.
---
 src/backend/postmaster/autovacuum.c      |  1 +
 src/backend/storage/lmgr/proc.c          |  1 +
 src/backend/tcop/postgres.c              | 17 +++++++++++++++++
 src/backend/utils/errcodes.txt           |  1 +
 src/backend/utils/init/postinit.c        | 13 +++++++++++++
 src/backend/utils/misc/guc_tables.c      | 11 +++++++++++
 src/bin/pg_dump/pg_backup_archiver.c     |  1 +
 src/bin/pg_dump/pg_dump.c                |  2 ++
 src/bin/pg_rewind/libpq_source.c         |  1 +
 src/include/miscadmin.h                  |  1 +
 src/include/storage/proc.h               |  1 +
 src/include/utils/timeout.h              |  1 +
 src/test/isolation/expected/timeouts.out | 18 ++++++++++++++++++
 src/test/isolation/specs/timeouts.spec   |  5 +++++
 14 files changed, 74 insertions(+)

diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 601834d4b4..828a28af0a 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -588,6 +588,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b1c35653fc..0170e226d0 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -61,6 +61,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 3082093d1e..41f3d19e95 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2691,6 +2691,9 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -2720,6 +2723,7 @@ finish_xact_command(void)
 
 	if (xact_started)
 	{
+
 		CommitTransactionCommand();
 
 #ifdef MEMORY_CONTEXT_CHECKING
@@ -3268,6 +3272,7 @@ ProcessInterrupts(void)
 	{
 		bool		lock_timeout_occurred;
 		bool		stmt_timeout_occurred;
+		bool		tx_timeout_occurred;
 
 		QueryCancelPending = false;
 
@@ -3277,6 +3282,7 @@ ProcessInterrupts(void)
 		 */
 		lock_timeout_occurred = get_timeout_indicator(LOCK_TIMEOUT, true);
 		stmt_timeout_occurred = get_timeout_indicator(STATEMENT_TIMEOUT, true);
+		tx_timeout_occurred = get_timeout_indicator(TRANSACTION_TIMEOUT, true);
 
 		/*
 		 * If both were set, we want to report whichever timeout completed
@@ -3302,6 +3308,13 @@ ProcessInterrupts(void)
 					(errcode(ERRCODE_QUERY_CANCELED),
 					 errmsg("canceling statement due to statement timeout")));
 		}
+		if (tx_timeout_occurred)
+		{
+			LockErrorCleanup();
+			ereport(ERROR,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("canceling transaction due to transaction timeout")));
+		}
 		if (IsAutoVacuumWorkerProcess())
 		{
 			LockErrorCleanup();
@@ -4460,6 +4473,10 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 62418a051a..3ae2bbda70 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index a990c833c5..56d11b5e62 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -756,6 +757,8 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT,
+						TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1360,6 +1363,16 @@ IdleInTransactionSessionTimeoutHandler(void)
 	SetLatch(MyLatch);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+#ifdef HAVE_SETSID
+	/* try to signal whole process group */
+	kill(-MyProcPid, SIGINT);
+#endif
+	kill(MyProcPid, SIGINT);
+}
+
 static void
 IdleSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1bf14eec66..ca21d2544c 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2452,6 +2452,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed in a transaction."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 0081873a72..5229fe3555 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3089,6 +3089,7 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 44e8cd4704..87be74753d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1188,6 +1188,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 160000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 011c9cce6e..1b9674140a 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 795182fa51..484384b4ed 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index aa13e1d66e..a892c72765 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -428,6 +428,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index c068986d09..c8fa0dc3d9 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..d07a2fae02 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -79,3 +79,21 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: tto sleep0 sleep0 sleep10
+step tto: SET transaction_timeout = '10ms';
+step sleep0: SELECT pg_sleep(0.0001)
+pg_sleep
+--------
+        
+(1 row)
+
+step sleep0: SELECT pg_sleep(0.0001)
+pg_sleep
+--------
+        
+(1 row)
+
+step sleep10: SELECT pg_sleep(0.01) <waiting ...>
+step sleep10: <... completed>
+ERROR:  canceling transaction due to transaction timeout
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..a7f27811c7 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -23,6 +23,9 @@ step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
 step slto	{ SET lock_timeout = '10s'; SET statement_timeout = '10ms'; }
+step tto	{ SET transaction_timeout = '10ms'; }
+step sleep0	{ SELECT pg_sleep(0.0001) }
+step sleep10	{ SELECT pg_sleep(0.01) }
 step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
@@ -47,3 +50,5 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+# transaction timeout
+permutation tto sleep0 sleep0 sleep10(*)
\ No newline at end of file
-- 
2.37.0 (Apple Git-136)



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2022-12-05 22:17  Nikolay Samokhvalov <[email protected]>
  parent: Andrey Borodin <[email protected]>
  2 siblings, 1 reply; 76+ messages in thread

From: Nikolay Samokhvalov @ 2022-12-05 22:17 UTC (permalink / raw)
  To: Andrey Borodin <[email protected]>; +Cc: pgsql-hackers

On Sat, Dec 3, 2022 at 9:41 AM Andrey Borodin <[email protected]> wrote:

> Fixed. Added test for this.
>

Thanks! Tested (gitpod:
https://gitpod.io/#https://gitlab.com/NikolayS/postgres/tree/transaction_timeout-v2
),

works as expected.


^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2022-12-05 22:28  Nikolay Samokhvalov <[email protected]>
  parent: Nikolay Samokhvalov <[email protected]>
  0 siblings, 0 replies; 76+ messages in thread

From: Nikolay Samokhvalov @ 2022-12-05 22:28 UTC (permalink / raw)
  To: [email protected]; +Cc: Andrey Borodin <[email protected]>

The following review has been posted through the commitfest application:
make installcheck-world:  tested, passed
Implements feature:       tested, passed
Spec compliant:           not tested
Documentation:            not tested

Tested, works as expected;

documentation is not yet added

^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2022-12-05 23:07  Andres Freund <[email protected]>
  parent: Andrey Borodin <[email protected]>
  2 siblings, 2 replies; 76+ messages in thread

From: Andres Freund @ 2022-12-05 23:07 UTC (permalink / raw)
  To: Andrey Borodin <[email protected]>; +Cc: Nikolay Samokhvalov <[email protected]>; pgsql-hackers

Hi,

On 2022-12-03 09:41:04 -0800, Andrey Borodin wrote:
> @@ -2720,6 +2723,7 @@ finish_xact_command(void)
>  
>  	if (xact_started)
>  	{
> +
>  		CommitTransactionCommand();
>  
>  #ifdef MEMORY_CONTEXT_CHECKING

Spurious newline added.


> @@ -4460,6 +4473,10 @@ PostgresMain(const char *dbname, const char *username)
>  					enable_timeout_after(IDLE_SESSION_TIMEOUT,
>  										 IdleSessionTimeout);
>  				}
> +
> +
> +				if (get_timeout_active(TRANSACTION_TIMEOUT))
> +					disable_timeout(TRANSACTION_TIMEOUT, false);
>  			}
>  
>  			/* Report any recently-changed GUC options */

Too many newlines added.


I'm a bit worried about adding evermore branches and function calls for
the processing of single statements. We already spend a noticable
percentage of the cycles for a single statement in PostgresMain(), this
adds additional overhead.

I'm somewhat inclined to think that we need some redesign here before we
add more overhead.


> @@ -1360,6 +1363,16 @@ IdleInTransactionSessionTimeoutHandler(void)
>  	SetLatch(MyLatch);
>  }
>  
> +static void
> +TransactionTimeoutHandler(void)
> +{
> +#ifdef HAVE_SETSID
> +	/* try to signal whole process group */
> +	kill(-MyProcPid, SIGINT);
> +#endif
> +	kill(MyProcPid, SIGINT);
> +}
> +

Why does this use signals instead of just setting the latch like
IdleInTransactionSessionTimeoutHandler() etc?



> diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
> index 0081873a72..5229fe3555 100644
> --- a/src/bin/pg_dump/pg_backup_archiver.c
> +++ b/src/bin/pg_dump/pg_backup_archiver.c
> @@ -3089,6 +3089,7 @@ _doSetFixedOutputState(ArchiveHandle *AH)
>  	ahprintf(AH, "SET statement_timeout = 0;\n");
>  	ahprintf(AH, "SET lock_timeout = 0;\n");
>  	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
> +	ahprintf(AH, "SET transaction_timeout = 0;\n");

Hm - why is that the right thing to do?



> diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
> index c747b4ae28..a7f27811c7 100644
> --- a/src/test/isolation/specs/timeouts.spec
> +++ b/src/test/isolation/specs/timeouts.spec
> @@ -23,6 +23,9 @@ step sto	{ SET statement_timeout = '10ms'; }
>  step lto	{ SET lock_timeout = '10ms'; }
>  step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
>  step slto	{ SET lock_timeout = '10s'; SET statement_timeout = '10ms'; }
> +step tto	{ SET transaction_timeout = '10ms'; }
> +step sleep0	{ SELECT pg_sleep(0.0001) }
> +step sleep10	{ SELECT pg_sleep(0.01) }
>  step locktbl	{ LOCK TABLE accounts; }
>  step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
>  teardown	{ ABORT; }
> @@ -47,3 +50,5 @@ permutation wrtbl lto update(*)
>  permutation wrtbl lsto update(*)
>  # statement timeout expires first, row-level lock
>  permutation wrtbl slto update(*)
> +# transaction timeout
> +permutation tto sleep0 sleep0 sleep10(*)
> \ No newline at end of file

I don't think this is quite sufficient. I think the test should verify
that transaction timeout interacts correctly with statement timeout /
idle in tx timeout.

Greetings,

Andres Freund





^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2022-12-05 23:41  Andrey Borodin <[email protected]>
  parent: Andres Freund <[email protected]>
  1 sibling, 1 reply; 76+ messages in thread

From: Andrey Borodin @ 2022-12-05 23:41 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Nikolay Samokhvalov <[email protected]>; pgsql-hackers

Thanks for looking into this Andres!

On Mon, Dec 5, 2022 at 3:07 PM Andres Freund <[email protected]> wrote:
>
> I'm a bit worried about adding evermore branches and function calls for
> the processing of single statements. We already spend a noticable
> percentage of the cycles for a single statement in PostgresMain(), this
> adds additional overhead.
>
> I'm somewhat inclined to think that we need some redesign here before we
> add more overhead.
>
We can cap statement_timeout\idle_session_timeout by the budget of
transaction_timeout left.
Either way we can do batch function enable_timeouts() instead
enable_timeout_after().

Does anything of it make sense?

>
> > @@ -1360,6 +1363,16 @@ IdleInTransactionSessionTimeoutHandler(void)
> >       SetLatch(MyLatch);
> >  }
> >
> > +static void
> > +TransactionTimeoutHandler(void)
> > +{
> > +#ifdef HAVE_SETSID
> > +     /* try to signal whole process group */
> > +     kill(-MyProcPid, SIGINT);
> > +#endif
> > +     kill(MyProcPid, SIGINT);
> > +}
> > +
>
> Why does this use signals instead of just setting the latch like
> IdleInTransactionSessionTimeoutHandler() etc?

I just copied statement_timeout behaviour. As I understand this
implementation is prefered if the timeout can catch the backend
running at full steam.

> > diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
> > index 0081873a72..5229fe3555 100644
> > --- a/src/bin/pg_dump/pg_backup_archiver.c
> > +++ b/src/bin/pg_dump/pg_backup_archiver.c
> > @@ -3089,6 +3089,7 @@ _doSetFixedOutputState(ArchiveHandle *AH)
> >       ahprintf(AH, "SET statement_timeout = 0;\n");
> >       ahprintf(AH, "SET lock_timeout = 0;\n");
> >       ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
> > +     ahprintf(AH, "SET transaction_timeout = 0;\n");
>
> Hm - why is that the right thing to do?
Because transaction_timeout has effects of statement_timeout.

Thank you!

Best regards, Andrey Borodin.





^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2022-12-06 00:22  Andres Freund <[email protected]>
  parent: Andrey Borodin <[email protected]>
  0 siblings, 0 replies; 76+ messages in thread

From: Andres Freund @ 2022-12-06 00:22 UTC (permalink / raw)
  To: Andrey Borodin <[email protected]>; Thomas Munro <[email protected]>; +Cc: Nikolay Samokhvalov <[email protected]>; pgsql-hackers

Hi,

On 2022-12-05 15:41:29 -0800, Andrey Borodin wrote:
> Thanks for looking into this Andres!
>
> On Mon, Dec 5, 2022 at 3:07 PM Andres Freund <[email protected]> wrote:
> >
> > I'm a bit worried about adding evermore branches and function calls for
> > the processing of single statements. We already spend a noticable
> > percentage of the cycles for a single statement in PostgresMain(), this
> > adds additional overhead.
> >
> > I'm somewhat inclined to think that we need some redesign here before we
> > add more overhead.
> >
> We can cap statement_timeout\idle_session_timeout by the budget of
> transaction_timeout left.

I don't know what you mean by that.


> @@ -3277,6 +3282,7 @@ ProcessInterrupts(void)
>  		 */
>  		lock_timeout_occurred = get_timeout_indicator(LOCK_TIMEOUT, true);
>  		stmt_timeout_occurred = get_timeout_indicator(STATEMENT_TIMEOUT, true);
> +		tx_timeout_occurred = get_timeout_indicator(TRANSACTION_TIMEOUT, true);
>  
>  		/*
>  		 * If both were set, we want to report whichever timeout completed

This doesn't update the preceding comment, btw, which now reads oddly:

		/*
		 * If LOCK_TIMEOUT and STATEMENT_TIMEOUT indicators are both set, we
		 * need to clear both, so always fetch both.
		 */



> > > @@ -1360,6 +1363,16 @@ IdleInTransactionSessionTimeoutHandler(void)
> > >       SetLatch(MyLatch);
> > >  }
> > >
> > > +static void
> > > +TransactionTimeoutHandler(void)
> > > +{
> > > +#ifdef HAVE_SETSID
> > > +     /* try to signal whole process group */
> > > +     kill(-MyProcPid, SIGINT);
> > > +#endif
> > > +     kill(MyProcPid, SIGINT);
> > > +}
> > > +
> >
> > Why does this use signals instead of just setting the latch like
> > IdleInTransactionSessionTimeoutHandler() etc?
>
> I just copied statement_timeout behaviour. As I understand this
> implementation is prefered if the timeout can catch the backend
> running at full steam.

Hm. I'm not particularly convinced by that code. Be that as it may, I
don't think it's a good idea to have one more copy of this code. At
least the patch should wrap the signalling code in a helper.


FWIW, the HAVE_SETSID code originates in:

commit 3ad0728c817bf8abd2c76bd11d856967509b307c
Author: Tom Lane <[email protected]>
Date:   2006-11-21 20:59:53 +0000

    On systems that have setsid(2) (which should be just about everything except
    Windows), arrange for each postmaster child process to be its own process
    group leader, and deliver signals SIGINT, SIGTERM, SIGQUIT to the whole
    process group not only the direct child process.  This provides saner behavior
    for archive and recovery scripts; in particular, it's possible to shut down a
    warm-standby recovery server using "pg_ctl stop -m immediate", since delivery
    of SIGQUIT to the startup subprocess will result in killing the waiting
    recovery_command.  Also, this makes Query Cancel and statement_timeout apply
    to scripts being run from backends via system().  (There is no support in the
    core backend for that, but it's widely done using untrusted PLs.)  Per gripe
    from Stephen Harris and subsequent discussion.



> > > diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
> > > index 0081873a72..5229fe3555 100644
> > > --- a/src/bin/pg_dump/pg_backup_archiver.c
> > > +++ b/src/bin/pg_dump/pg_backup_archiver.c
> > > @@ -3089,6 +3089,7 @@ _doSetFixedOutputState(ArchiveHandle *AH)
> > >       ahprintf(AH, "SET statement_timeout = 0;\n");
> > >       ahprintf(AH, "SET lock_timeout = 0;\n");
> > >       ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
> > > +     ahprintf(AH, "SET transaction_timeout = 0;\n");
> >
> > Hm - why is that the right thing to do?
> Because transaction_timeout has effects of statement_timeout.

I guess it's just following precedent - but it seems a bit presumptuous
to just disable safety settings a DBA might have set up. That makes some
sense for e.g. idle_in_transaction_session_timeout, because I think
e.g. parallel backup can lead to a connection being idle for a bit.


A few more review comments:

> Either way we can do batch function enable_timeouts() instead
> enable_timeout_after().

> Does anything of it make sense?

I'm at least as worried about the various calls *after* the execution of
a statement.


> +		if (tx_timeout_occurred)
> +		{
> +			LockErrorCleanup();
> +			ereport(ERROR,
> +					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
> +					 errmsg("canceling transaction due to transaction timeout")));
> +		}

The number of calls to LockErrorCleanup() here feels wrong - there's
already 8 calls in ProcessInterrupts(). Besides the code duplication I
also think it's not a sane idea to rely on having LockErrorCleanup()
before all the relevant ereport(ERROR)s.

Greetings,

Andres Freund





^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2022-12-06 00:44  Kyotaro Horiguchi <[email protected]>
  parent: Andres Freund <[email protected]>
  1 sibling, 1 reply; 76+ messages in thread

From: Kyotaro Horiguchi @ 2022-12-06 00:44 UTC (permalink / raw)
  To: [email protected]; +Cc: [email protected]; [email protected]; pgsql-hackers

At Mon, 5 Dec 2022 15:07:47 -0800, Andres Freund <[email protected]> wrote in 
> I'm a bit worried about adding evermore branches and function calls for
> the processing of single statements. We already spend a noticable
> percentage of the cycles for a single statement in PostgresMain(), this
> adds additional overhead.
> 
> I'm somewhat inclined to think that we need some redesign here before we
> add more overhead.

insert_timeout() and remove_timeout_index() move 40*(# of several
timeouts) bytes at every enabling/disabling a timeout. This is far
frequent than actually any timeout fires. schedule_alarm() is
interested only in the nearest timeout.

So, we can get rid of the insertion sort in
insert_timeout/remove_timeout_index then let them just search for the
nearest one and remember it. Finding the nearest should be faster than
the insertion sort. Instead we need to scan over the all timeouts
instead of the a few first ones, but that's overhead is not a matter
when a timeout fires.

regards.

-- 
Kyotaro Horiguchi
NTT Open Source Software Center





^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2022-12-06 01:10  Andres Freund <[email protected]>
  parent: Kyotaro Horiguchi <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andres Freund @ 2022-12-06 01:10 UTC (permalink / raw)
  To: Kyotaro Horiguchi <[email protected]>; +Cc: [email protected]; [email protected]; pgsql-hackers

Hi,

On 2022-12-06 09:44:01 +0900, Kyotaro Horiguchi wrote:
> At Mon, 5 Dec 2022 15:07:47 -0800, Andres Freund <[email protected]> wrote in
> > I'm a bit worried about adding evermore branches and function calls for
> > the processing of single statements. We already spend a noticable
> > percentage of the cycles for a single statement in PostgresMain(), this
> > adds additional overhead.
> >
> > I'm somewhat inclined to think that we need some redesign here before we
> > add more overhead.
>
> insert_timeout() and remove_timeout_index() move 40*(# of several
> timeouts) bytes at every enabling/disabling a timeout. This is far
> frequent than actually any timeout fires. schedule_alarm() is
> interested only in the nearest timeout.

> So, we can get rid of the insertion sort in
> insert_timeout/remove_timeout_index then let them just search for the
> nearest one and remember it. Finding the nearest should be faster than
> the insertion sort. Instead we need to scan over the all timeouts
> instead of the a few first ones, but that's overhead is not a matter
> when a timeout fires.

I'm most concerned about the overhead when the timeouts are *not*
enabled. And this adds a branch to start_xact_command() and a function
call for get_timeout_active(TRANSACTION_TIMEOUT) in that case. On its
own, that's not a whole lot, but it does add up. There's 10+ function
calls for timeout and ps_display purposes for every single statement.

But it's definitely also worth optimizing the timeout enabled paths. And
you're right, it looks like there's a fair bit of optimization
potential.

Greetings,

Andres Freund





^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2022-12-07 02:52  Kyotaro Horiguchi <[email protected]>
  parent: Andres Freund <[email protected]>
  0 siblings, 0 replies; 76+ messages in thread

From: Kyotaro Horiguchi @ 2022-12-07 02:52 UTC (permalink / raw)
  To: [email protected]; +Cc: [email protected]; [email protected]; pgsql-hackers

At Mon, 5 Dec 2022 17:10:50 -0800, Andres Freund <[email protected]> wrote in 
> I'm most concerned about the overhead when the timeouts are *not*
> enabled. And this adds a branch to start_xact_command() and a function
> call for get_timeout_active(TRANSACTION_TIMEOUT) in that case. On its
> own, that's not a whole lot, but it does add up. There's 10+ function
> calls for timeout and ps_display purposes for every single statement.

That path seems like existing just for robustness. I inserted
"Assert(0)" just before the disable_timeout(), but make check-world
didn't fail [1]. Couldn't we get rid of that path, adding an assertion
instead?  I'm not sure about other timeouts yet, though.

About disabling side, we cannot rely on StatementTimeout.

[1]
# 032_apply_delay.pl fails for me so I don't know any of the later
# tests fails.

> But it's definitely also worth optimizing the timeout enabled paths. And
> you're right, it looks like there's a fair bit of optimization
> potential.

Thanks. I'll work on that.

regards.

-- 
Kyotaro Horiguchi
NTT Open Source Software Center





^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2022-12-07 18:23  Andres Freund <[email protected]>
  parent: Andrey Borodin <[email protected]>
  2 siblings, 1 reply; 76+ messages in thread

From: Andres Freund @ 2022-12-07 18:23 UTC (permalink / raw)
  To: Andrey Borodin <[email protected]>; Michael Paquier <[email protected]>; +Cc: Nikolay Samokhvalov <[email protected]>; pgsql-hackers

Hi,

On 2022-12-03 09:41:04 -0800, Andrey Borodin wrote:
> Fixed. Added test for this.

The tests don't pass: https://cirrus-ci.com/build/4811553145356288

[00:54:35.337](1.251s) not ok 1 - no parameters missing from postgresql.conf.sample
[00:54:35.338](0.000s) #   Failed test 'no parameters missing from postgresql.conf.sample'
#   at /tmp/cirrus-ci-build/src/test/modules/test_misc/t/003_check_guc.pl line 81.
[00:54:35.338](0.000s) #          got: '1'
#     expected: '0'


I am just looking through a bunch of failing CF entries, so I'm perhaps
over-sensitized right now. But I'm a bit confused why there's so many
occasions of the tests clearly not having been run...


Michael, any reason 003_check_guc doesn't show the missing GUCs? It's not
particularly helpful to see "0 is different from 1". Seems that even just
something like
  is_deeply(\@missing_from_file, [], "no parameters missing from postgresql.conf.sample");
would be a decent improvement?

Greetings,

Andres Freund





^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2022-12-07 21:30  Andrey Borodin <[email protected]>
  parent: Andres Freund <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey Borodin @ 2022-12-07 21:30 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers

On Wed, Dec 7, 2022 at 10:23 AM Andres Freund <[email protected]> wrote:
>
> On 2022-12-03 09:41:04 -0800, Andrey Borodin wrote:
> > Fixed. Added test for this.
>
> The tests don't pass: https://cirrus-ci.com/build/4811553145356288
>
oops, sorry. Here's the fix. I hope to address other feedback on the
weekend. Thank you!

Best regards, Andrey Borodin.


Attachments:

  [application/octet-stream] v3-0001-Intorduce-transaction_timeout.patch (12.5K, ../../CAAhFRxgP_w8ddYPJ3cJ4QZKQU7VWrH55MOYrxdceFBchDwuGrQ@mail.gmail.com/2-v3-0001-Intorduce-transaction_timeout.patch)
  download | inline diff:
From 026cdc68e138918ddc927e7f779dca98a1eea30b Mon Sep 17 00:00:00 2001
From: Andrey Borodin <[email protected]>
Date: Fri, 2 Dec 2022 21:01:29 -0800
Subject: [PATCH v3] Intorduce transaction_timeout

Just like statement_timeout, but for transaction.
---
 src/backend/postmaster/autovacuum.c           |  1 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 17 +++++++++++++++++
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/postinit.c             | 13 +++++++++++++
 src/backend/utils/misc/guc_tables.c           | 11 +++++++++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  1 +
 src/bin/pg_dump/pg_dump.c                     |  2 ++
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/expected/timeouts.out      | 18 ++++++++++++++++++
 src/test/isolation/specs/timeouts.spec        |  5 +++++
 15 files changed, 75 insertions(+)

diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 601834d4b4..828a28af0a 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -588,6 +588,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b1c35653fc..0170e226d0 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -61,6 +61,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 3082093d1e..41f3d19e95 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2691,6 +2691,9 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -2720,6 +2723,7 @@ finish_xact_command(void)
 
 	if (xact_started)
 	{
+
 		CommitTransactionCommand();
 
 #ifdef MEMORY_CONTEXT_CHECKING
@@ -3268,6 +3272,7 @@ ProcessInterrupts(void)
 	{
 		bool		lock_timeout_occurred;
 		bool		stmt_timeout_occurred;
+		bool		tx_timeout_occurred;
 
 		QueryCancelPending = false;
 
@@ -3277,6 +3282,7 @@ ProcessInterrupts(void)
 		 */
 		lock_timeout_occurred = get_timeout_indicator(LOCK_TIMEOUT, true);
 		stmt_timeout_occurred = get_timeout_indicator(STATEMENT_TIMEOUT, true);
+		tx_timeout_occurred = get_timeout_indicator(TRANSACTION_TIMEOUT, true);
 
 		/*
 		 * If both were set, we want to report whichever timeout completed
@@ -3302,6 +3308,13 @@ ProcessInterrupts(void)
 					(errcode(ERRCODE_QUERY_CANCELED),
 					 errmsg("canceling statement due to statement timeout")));
 		}
+		if (tx_timeout_occurred)
+		{
+			LockErrorCleanup();
+			ereport(ERROR,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("canceling transaction due to transaction timeout")));
+		}
 		if (IsAutoVacuumWorkerProcess())
 		{
 			LockErrorCleanup();
@@ -4460,6 +4473,10 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 62418a051a..3ae2bbda70 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index a990c833c5..56d11b5e62 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -756,6 +757,8 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT,
+						TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1360,6 +1363,16 @@ IdleInTransactionSessionTimeoutHandler(void)
 	SetLatch(MyLatch);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+#ifdef HAVE_SETSID
+	/* try to signal whole process group */
+	kill(-MyProcPid, SIGINT);
+#endif
+	kill(MyProcPid, SIGINT);
+}
+
 static void
 IdleSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1bf14eec66..ca21d2544c 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2452,6 +2452,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed in a transaction."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 043864597f..c9c9bc6633 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -688,6 +688,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0			# in milliseconds, 0 is disabled
+#transaction_timeout = 0		# in milliseconds, 0 is disabled
 #lock_timeout = 0			# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0		# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 0081873a72..5229fe3555 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3089,6 +3089,7 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 44e8cd4704..87be74753d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1188,6 +1188,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 160000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 011c9cce6e..1b9674140a 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 795182fa51..484384b4ed 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index aa13e1d66e..a892c72765 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -428,6 +428,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index c068986d09..c8fa0dc3d9 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..d07a2fae02 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -79,3 +79,21 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: tto sleep0 sleep0 sleep10
+step tto: SET transaction_timeout = '10ms';
+step sleep0: SELECT pg_sleep(0.0001)
+pg_sleep
+--------
+        
+(1 row)
+
+step sleep0: SELECT pg_sleep(0.0001)
+pg_sleep
+--------
+        
+(1 row)
+
+step sleep10: SELECT pg_sleep(0.01) <waiting ...>
+step sleep10: <... completed>
+ERROR:  canceling transaction due to transaction timeout
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..a7f27811c7 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -23,6 +23,9 @@ step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
 step slto	{ SET lock_timeout = '10s'; SET statement_timeout = '10ms'; }
+step tto	{ SET transaction_timeout = '10ms'; }
+step sleep0	{ SELECT pg_sleep(0.0001) }
+step sleep10	{ SELECT pg_sleep(0.01) }
 step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
@@ -47,3 +50,5 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+# transaction timeout
+permutation tto sleep0 sleep0 sleep10(*)
\ No newline at end of file
-- 
2.37.0 (Apple Git-136)



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2022-12-18 20:53  Andrey Borodin <[email protected]>
  parent: Andrey Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey Borodin @ 2022-12-18 20:53 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers

On Wed, Dec 7, 2022 at 1:30 PM Andrey Borodin <[email protected]> wrote:
> I hope to address other feedback on the weekend.

Andres, here's my progress on working with your review notes.

> > @@ -3277,6 +3282,7 @@ ProcessInterrupts(void)
> >                */
> >               lock_timeout_occurred = get_timeout_indicator(LOCK_TIMEOUT, true);
> >               stmt_timeout_occurred = get_timeout_indicator(STATEMENT_TIMEOUT, true);
> > +             tx_timeout_occurred = get_timeout_indicator(TRANSACTION_TIMEOUT, true);
> >
> >               /*
> >                * If both were set, we want to report whichever timeout completed
>
> This doesn't update the preceding comment, btw, which now reads oddly:

I've rewritten this part to correctly report all timeouts that did
happen. However there's now a tricky comma-formatting code which was
tested only manually.

> > > > @@ -1360,6 +1363,16 @@ IdleInTransactionSessionTimeoutHandler(void)
> > > >       SetLatch(MyLatch);
> > > >  }
> > > >
> > > > +static void
> > > > +TransactionTimeoutHandler(void)
> > > > +{
> > > > +#ifdef HAVE_SETSID
> > > > +     /* try to signal whole process group */
> > > > +     kill(-MyProcPid, SIGINT);
> > > > +#endif
> > > > +     kill(MyProcPid, SIGINT);
> > > > +}
> > > > +
> > >
> > > Why does this use signals instead of just setting the latch like
> > > IdleInTransactionSessionTimeoutHandler() etc?
> >
> > I just copied statement_timeout behaviour. As I understand this
> > implementation is prefered if the timeout can catch the backend
> > running at full steam.
>
> Hm. I'm not particularly convinced by that code. Be that as it may, I
> don't think it's a good idea to have one more copy of this code. At
> least the patch should wrap the signalling code in a helper.

Done, now there is a single CancelOnTimeoutHandler() handler.

> > > > diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
> > > > index 0081873a72..5229fe3555 100644
> > > > --- a/src/bin/pg_dump/pg_backup_archiver.c
> > > > +++ b/src/bin/pg_dump/pg_backup_archiver.c
> > > > @@ -3089,6 +3089,7 @@ _doSetFixedOutputState(ArchiveHandle *AH)
> > > >       ahprintf(AH, "SET statement_timeout = 0;\n");
> > > >       ahprintf(AH, "SET lock_timeout = 0;\n");
> > > >       ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
> > > > +     ahprintf(AH, "SET transaction_timeout = 0;\n");
> > >
> > > Hm - why is that the right thing to do?
> > Because transaction_timeout has effects of statement_timeout.
>
> I guess it's just following precedent - but it seems a bit presumptuous
> to just disable safety settings a DBA might have set up. That makes some
> sense for e.g. idle_in_transaction_session_timeout, because I think
> e.g. parallel backup can lead to a connection being idle for a bit.

I do not know. My reasoning - everywhere we turn off
statement_timeout, we should turn off transaction_timeout too.
But I have no strong opinion here. I left this code as is in the patch
so far. For the same reason I did not change anything in
pg_backup_archiver.c.

> > Either way we can do batch function enable_timeouts() instead
> > enable_timeout_after().
>
> > Does anything of it make sense?
>
> I'm at least as worried about the various calls *after* the execution of
> a statement.

I think this code is just a one bit check
if (get_timeout_active(TRANSACTION_TIMEOUT))
inside of get_timeout_active(). With all 14 timeouts we have, I don't
see a good way to optimize stuff so far.

> > +             if (tx_timeout_occurred)
> > +             {
> > +                     LockErrorCleanup();
> > +                     ereport(ERROR,
> > +                                     (errcode(ERRCODE_TRANSACTION_TIMEOUT),
> > +                                      errmsg("canceling transaction due to transaction timeout")));
> > +             }
>
> The number of calls to LockErrorCleanup() here feels wrong - there's
> already 8 calls in ProcessInterrupts(). Besides the code duplication I
> also think it's not a sane idea to rely on having LockErrorCleanup()
> before all the relevant ereport(ERROR)s.

I've refactored that code down to 7 calls of LockErrorCleanup() :)
Logic behind various branches is not clear for me, e.g. why we do not
LockErrorCleanup() when reading commands from a client?
So I did not risk refactoring further.

> I think the test should verify
> that transaction timeout interacts correctly with statement timeout /
> idle in tx timeout.

I've added tests that check statement_timeout vs transaction_timeout.
However I could not produce stable tests with
idle_in_transaction_timeout vs transaction_timeout so far. But I'll
look into this more.
Actually, stabilizing statement_timeout vs transaction_timeout was
tricky on Windows too. I had to remove the second call to
pg_sleep(0.0001) because it was triggering 10ьs timeout from time to
time. Also, test timeout was increased to 30ms, because unlike others
in spec it's not supposed to happen at the very first SQL statement.

Thank you!


Best regards, Andrey Borodin.


Attachments:

  [application/octet-stream] v4-0001-Intorduce-transaction_timeout.patch (16.6K, ../../CAAhFRxg9i6DrHT1cALBNztsnJ7OR3cawfCmyFA=J6=oDFX+h2Q@mail.gmail.com/2-v4-0001-Intorduce-transaction_timeout.patch)
  download | inline diff:
From f75b18441db3a40f143ce094cb7e705ae34640db Mon Sep 17 00:00:00 2001
From: Andrey Borodin <[email protected]>
Date: Fri, 2 Dec 2022 21:01:29 -0800
Subject: [PATCH v4] Intorduce transaction_timeout

Just like statement_timeout, but for transaction.

Author: Andrey Borodin <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 24 +++++++++
 src/backend/postmaster/autovacuum.c           |  1 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 51 +++++++++++--------
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/postinit.c             |  9 ++--
 src/backend/utils/misc/guc_tables.c           | 11 ++++
 src/backend/utils/misc/postgresql.conf.sample |  7 +--
 src/bin/pg_dump/pg_backup_archiver.c          |  1 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/expected/timeouts.out      | 18 +++++++
 src/test/isolation/specs/timeouts.spec        |  8 +++
 16 files changed, 110 insertions(+), 28 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 39d1c89e33..fd74db7527 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9009,6 +9009,30 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Cancel any transaction that spans longer than the specified amount of
+        time. The limit applies both to explicit transactions (started with
+        <command>BEGIN</command>) and to implicitly started transaction
+        corresponding to single statement.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> 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>)
       <indexterm>
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 601834d4b4..828a28af0a 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -588,6 +588,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b1c35653fc..0170e226d0 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -61,6 +61,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 3082093d1e..1eaf2e054a 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2691,6 +2691,9 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -3268,40 +3271,43 @@ ProcessInterrupts(void)
 	{
 		bool		lock_timeout_occurred;
 		bool		stmt_timeout_occurred;
+		bool		tx_timeout_occurred;
+		int			err_code = ERRCODE_QUERY_CANCELED;
 
 		QueryCancelPending = false;
 
 		/*
-		 * If LOCK_TIMEOUT and STATEMENT_TIMEOUT indicators are both set, we
-		 * need to clear both, so always fetch both.
+		 * If LOCK_TIMEOUT, STATEMENT_TIMEOUT and TRANSACTION indicators are set, we
+		 * need to clear all of them, so always fetch each one.
 		 */
 		lock_timeout_occurred = get_timeout_indicator(LOCK_TIMEOUT, true);
 		stmt_timeout_occurred = get_timeout_indicator(STATEMENT_TIMEOUT, true);
-
-		/*
-		 * If both were set, we want to report whichever timeout completed
-		 * earlier; this ensures consistent behavior if the machine is slow
-		 * enough that the second timeout triggers before we get here.  A tie
-		 * is arbitrarily broken in favor of reporting a lock timeout.
-		 */
-		if (lock_timeout_occurred && stmt_timeout_occurred &&
-			get_timeout_finish_time(STATEMENT_TIMEOUT) < get_timeout_finish_time(LOCK_TIMEOUT))
-			lock_timeout_occurred = false;	/* report stmt timeout */
+		tx_timeout_occurred = get_timeout_indicator(TRANSACTION_TIMEOUT, true);
 
 		if (lock_timeout_occurred)
+			err_code = ERRCODE_LOCK_NOT_AVAILABLE;
+
+
+		if (lock_timeout_occurred || stmt_timeout_occurred || tx_timeout_occurred)
 		{
+			/* Report all reasons for timeout */
+			char* lock_reason = lock_timeout_occurred ?
+									_("lock timeout") : "";
+			char* stmt_reason = stmt_timeout_occurred ?
+									_("statement timeout") : "";
+			char* tx_reason = tx_timeout_occurred ?
+									_("transaction timeout") : "";
+			char* comma1 = lock_timeout_occurred && stmt_timeout_occurred ?
+									"," : "";
+			char* comma2 = (lock_timeout_occurred || stmt_timeout_occurred)
+									&& tx_timeout_occurred ? "," : "";
 			LockErrorCleanup();
 			ereport(ERROR,
-					(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-					 errmsg("canceling statement due to lock timeout")));
-		}
-		if (stmt_timeout_occurred)
-		{
-			LockErrorCleanup();
-			ereport(ERROR,
-					(errcode(ERRCODE_QUERY_CANCELED),
-					 errmsg("canceling statement due to statement timeout")));
+					(errcode(err_code),
+					 errmsg("canceling statement due to %s%s%s%s%s", lock_reason, comma1,
+					 			stmt_reason, comma2, tx_reason)));
 		}
+
 		if (IsAutoVacuumWorkerProcess())
 		{
 			LockErrorCleanup();
@@ -4460,6 +4466,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 62418a051a..3ae2bbda70 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index a990c833c5..779cb22915 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -73,7 +73,7 @@ static void PerformAuthentication(Port *port);
 static void CheckMyDatabase(const char *name, bool am_superuser, bool override_allow_connections);
 static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
-static void LockTimeoutHandler(void);
+static void CancelOnTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
@@ -753,9 +753,10 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	{
 		RegisterTimeout(DEADLOCK_TIMEOUT, CheckDeadLockAlert);
 		RegisterTimeout(STATEMENT_TIMEOUT, StatementTimeoutHandler);
-		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
+		RegisterTimeout(LOCK_TIMEOUT, CancelOnTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, CancelOnTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1340,10 +1341,10 @@ StatementTimeoutHandler(void)
 }
 
 /*
- * LOCK_TIMEOUT handler: trigger a query-cancel interrupt.
+ * LOCK_TIMEOUT and TRANSACTION_TIMEOUT handler: trigger a query-cancel interrupt.
  */
 static void
-LockTimeoutHandler(void)
+CancelOnTimeoutHandler(void)
 {
 #ifdef HAVE_SETSID
 	/* try to signal whole process group */
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1bf14eec66..ca21d2544c 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2452,6 +2452,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed in a transaction."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 043864597f..a8c935f60c 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -687,10 +687,11 @@
 #default_transaction_read_only = off
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
-#statement_timeout = 0			# in milliseconds, 0 is disabled
-#lock_timeout = 0			# in milliseconds, 0 is disabled
+#statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
+#lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
-#idle_session_timeout = 0		# in milliseconds, 0 is disabled
+#idle_session_timeout = 0			# in milliseconds, 0 is disabled
 #vacuum_freeze_table_age = 150000000
 #vacuum_freeze_min_age = 50000000
 #vacuum_failsafe_age = 1600000000
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 0081873a72..5229fe3555 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3089,6 +3089,7 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 44e8cd4704..87be74753d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1188,6 +1188,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 160000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 011c9cce6e..1b9674140a 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 795182fa51..484384b4ed 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index aa13e1d66e..a892c72765 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -428,6 +428,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index c068986d09..c8fa0dc3d9 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..6325ab4d00 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -79,3 +79,21 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: tsto sleep0 sleep1000
+step tsto: SET transaction_timeout = '30ms'; SET statement_timeout = '10s';
+step sleep0: SELECT pg_sleep(0.0001)
+pg_sleep
+--------
+        
+(1 row)
+
+step sleep1000: SELECT pg_sleep(1000) <waiting ...>
+step sleep1000: <... completed>
+ERROR:  canceling statement due to transaction timeout
+
+starting permutation: stto sleep1000
+step stto: SET transaction_timeout = '10s'; SET statement_timeout = '10ms';
+step sleep1000: SELECT pg_sleep(1000) <waiting ...>
+step sleep1000: <... completed>
+ERROR:  canceling statement due to statement timeout
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..82d7fc501a 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -23,6 +23,10 @@ step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
 step slto	{ SET lock_timeout = '10s'; SET statement_timeout = '10ms'; }
+step tsto	{ SET transaction_timeout = '30ms'; SET statement_timeout = '10s';}
+step stto	{ SET transaction_timeout = '10s'; SET statement_timeout = '10ms';}
+step sleep0	{ SELECT pg_sleep(0.0001) } # expected to always finish in time
+step sleep1000	{ SELECT pg_sleep(1000) } # expected to timeout always
 step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
@@ -47,3 +51,7 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+# transaction timeout before statement timeout
+permutation tsto sleep0 sleep1000(*)
+# statement timeout before transaction timeout
+permutation stto sleep1000(*)
\ No newline at end of file
-- 
2.37.0 (Apple Git-136)



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* [PATCH v4 1/2] rename some pgstat functions
@ 2023-08-24 14:42  Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 76+ messages in thread

From: Nathan Bossart @ 2023-08-24 14:42 UTC (permalink / raw)

---
 src/backend/utils/activity/backend_status.c | 28 ++++++++--------
 src/backend/utils/adt/pgstatfuncs.c         | 36 ++++++++++-----------
 src/include/utils/backend_status.h          |  4 +--
 3 files changed, 34 insertions(+), 34 deletions(-)

diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index 38f91a495b..a4860b10fc 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -849,8 +849,8 @@ pgstat_read_current_status(void)
 			/*
 			 * The BackendStatusArray index is exactly the BackendId of the
 			 * source backend.  Note that this means localBackendStatusTable
-			 * is in order by backend_id.  pgstat_fetch_stat_beentry() depends
-			 * on that.
+			 * is in order by backend_id.  pgstat_get_beentry_by_backend_id()
+			 * depends on that.
 			 */
 			localentry->backend_id = i;
 			BackendIdGetTransactionIds(i,
@@ -1073,21 +1073,21 @@ cmp_lbestatus(const void *a, const void *b)
 }
 
 /* ----------
- * pgstat_fetch_stat_beentry() -
+ * pgstat_get_beentry_by_backend_id() -
  *
  *	Support function for the SQL-callable pgstat* functions. Returns
  *	our local copy of the current-activity entry for one backend,
  *	or NULL if the given beid doesn't identify any known session.
  *
  *	The beid argument is the BackendId of the desired session
- *	(note that this is unlike pgstat_fetch_stat_local_beentry()).
+ *	(note that this is unlike pgstat_get_local_beentry_by_index()).
  *
  *	NB: caller is responsible for a check if the user is permitted to see
  *	this info (especially the querystring).
  * ----------
  */
 PgBackendStatus *
-pgstat_fetch_stat_beentry(BackendId beid)
+pgstat_get_beentry_by_backend_id(BackendId beid)
 {
 	LocalPgBackendStatus key;
 	LocalPgBackendStatus *ret;
@@ -1111,13 +1111,13 @@ pgstat_fetch_stat_beentry(BackendId beid)
 
 
 /* ----------
- * pgstat_fetch_stat_local_beentry() -
+ * pgstat_get_local_beentry_by_index() -
  *
- *	Like pgstat_fetch_stat_beentry() but with locally computed additions (like
- *	xid and xmin values of the backend)
+ *	Like pgstat_get_beentry_by_backend_id() but with locally computed additions
+ *	(like xid and xmin values of the backend)
  *
- *	The beid argument is a 1-based index in the localBackendStatusTable
- *	(note that this is unlike pgstat_fetch_stat_beentry()).
+ *	The idx argument is a 1-based index in the localBackendStatusTable
+ *	(note that this is unlike pgstat_get_beentry_by_backend_id()).
  *	Returns NULL if the argument is out of range (no current caller does that).
  *
  *	NB: caller is responsible for a check if the user is permitted to see
@@ -1125,14 +1125,14 @@ pgstat_fetch_stat_beentry(BackendId beid)
  * ----------
  */
 LocalPgBackendStatus *
-pgstat_fetch_stat_local_beentry(int beid)
+pgstat_get_local_beentry_by_index(int idx)
 {
 	pgstat_read_current_status();
 
-	if (beid < 1 || beid > localNumBackends)
+	if (idx < 1 || idx > localNumBackends)
 		return NULL;
 
-	return &localBackendStatusTable[beid - 1];
+	return &localBackendStatusTable[idx - 1];
 }
 
 
@@ -1141,7 +1141,7 @@ pgstat_fetch_stat_local_beentry(int beid)
  *
  *	Support function for the SQL-callable pgstat* functions. Returns
  *	the number of sessions known in the localBackendStatusTable, i.e.
- *	the maximum 1-based index to pass to pgstat_fetch_stat_local_beentry().
+ *	the maximum 1-based index to pass to pgstat_get_local_beentry_by_index().
  * ----------
  */
 int
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 2b9742ad21..49cc887b97 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -211,7 +211,7 @@ pg_stat_get_backend_idset(PG_FUNCTION_ARGS)
 	if (fctx[0] <= pgstat_fetch_stat_numbackends())
 	{
 		/* do when there is more left to send */
-		LocalPgBackendStatus *local_beentry = pgstat_fetch_stat_local_beentry(fctx[0]);
+		LocalPgBackendStatus *local_beentry = pgstat_get_local_beentry_by_index(fctx[0]);
 
 		SRF_RETURN_NEXT(funcctx, Int32GetDatum(local_beentry->backend_id));
 	}
@@ -264,7 +264,7 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
 		bool		nulls[PG_STAT_GET_PROGRESS_COLS] = {0};
 		int			i;
 
-		local_beentry = pgstat_fetch_stat_local_beentry(curr_backend);
+		local_beentry = pgstat_get_local_beentry_by_index(curr_backend);
 		beentry = &local_beentry->backendStatus;
 
 		/*
@@ -325,7 +325,7 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
 		const char *wait_event = NULL;
 
 		/* Get the next one in the list */
-		local_beentry = pgstat_fetch_stat_local_beentry(curr_backend);
+		local_beentry = pgstat_get_local_beentry_by_index(curr_backend);
 		beentry = &local_beentry->backendStatus;
 
 		/* If looking for specific PID, ignore all the others */
@@ -672,7 +672,7 @@ pg_stat_get_backend_pid(PG_FUNCTION_ARGS)
 	int32		beid = PG_GETARG_INT32(0);
 	PgBackendStatus *beentry;
 
-	if ((beentry = pgstat_fetch_stat_beentry(beid)) == NULL)
+	if ((beentry = pgstat_get_beentry_by_backend_id(beid)) == NULL)
 		PG_RETURN_NULL();
 
 	PG_RETURN_INT32(beentry->st_procpid);
@@ -685,7 +685,7 @@ pg_stat_get_backend_dbid(PG_FUNCTION_ARGS)
 	int32		beid = PG_GETARG_INT32(0);
 	PgBackendStatus *beentry;
 
-	if ((beentry = pgstat_fetch_stat_beentry(beid)) == NULL)
+	if ((beentry = pgstat_get_beentry_by_backend_id(beid)) == NULL)
 		PG_RETURN_NULL();
 
 	PG_RETURN_OID(beentry->st_databaseid);
@@ -698,7 +698,7 @@ pg_stat_get_backend_userid(PG_FUNCTION_ARGS)
 	int32		beid = PG_GETARG_INT32(0);
 	PgBackendStatus *beentry;
 
-	if ((beentry = pgstat_fetch_stat_beentry(beid)) == NULL)
+	if ((beentry = pgstat_get_beentry_by_backend_id(beid)) == NULL)
 		PG_RETURN_NULL();
 
 	PG_RETURN_OID(beentry->st_userid);
@@ -727,7 +727,7 @@ pg_stat_get_backend_subxact(PG_FUNCTION_ARGS)
 
 	BlessTupleDesc(tupdesc);
 
-	if ((local_beentry = pgstat_fetch_stat_local_beentry(beid)) != NULL)
+	if ((local_beentry = pgstat_get_local_beentry_by_index(beid)) != NULL)
 	{
 		/* Fill values and NULLs */
 		values[0] = Int32GetDatum(local_beentry->backend_subxact_count);
@@ -752,7 +752,7 @@ pg_stat_get_backend_activity(PG_FUNCTION_ARGS)
 	char	   *clipped_activity;
 	text	   *ret;
 
-	if ((beentry = pgstat_fetch_stat_beentry(beid)) == NULL)
+	if ((beentry = pgstat_get_beentry_by_backend_id(beid)) == NULL)
 		activity = "<backend information not available>";
 	else if (!HAS_PGSTAT_PERMISSIONS(beentry->st_userid))
 		activity = "<insufficient privilege>";
@@ -776,7 +776,7 @@ pg_stat_get_backend_wait_event_type(PG_FUNCTION_ARGS)
 	PGPROC	   *proc;
 	const char *wait_event_type = NULL;
 
-	if ((beentry = pgstat_fetch_stat_beentry(beid)) == NULL)
+	if ((beentry = pgstat_get_beentry_by_backend_id(beid)) == NULL)
 		wait_event_type = "<backend information not available>";
 	else if (!HAS_PGSTAT_PERMISSIONS(beentry->st_userid))
 		wait_event_type = "<insufficient privilege>";
@@ -797,7 +797,7 @@ pg_stat_get_backend_wait_event(PG_FUNCTION_ARGS)
 	PGPROC	   *proc;
 	const char *wait_event = NULL;
 
-	if ((beentry = pgstat_fetch_stat_beentry(beid)) == NULL)
+	if ((beentry = pgstat_get_beentry_by_backend_id(beid)) == NULL)
 		wait_event = "<backend information not available>";
 	else if (!HAS_PGSTAT_PERMISSIONS(beentry->st_userid))
 		wait_event = "<insufficient privilege>";
@@ -818,7 +818,7 @@ pg_stat_get_backend_activity_start(PG_FUNCTION_ARGS)
 	TimestampTz result;
 	PgBackendStatus *beentry;
 
-	if ((beentry = pgstat_fetch_stat_beentry(beid)) == NULL)
+	if ((beentry = pgstat_get_beentry_by_backend_id(beid)) == NULL)
 		PG_RETURN_NULL();
 
 	else if (!HAS_PGSTAT_PERMISSIONS(beentry->st_userid))
@@ -844,7 +844,7 @@ pg_stat_get_backend_xact_start(PG_FUNCTION_ARGS)
 	TimestampTz result;
 	PgBackendStatus *beentry;
 
-	if ((beentry = pgstat_fetch_stat_beentry(beid)) == NULL)
+	if ((beentry = pgstat_get_beentry_by_backend_id(beid)) == NULL)
 		PG_RETURN_NULL();
 
 	else if (!HAS_PGSTAT_PERMISSIONS(beentry->st_userid))
@@ -866,7 +866,7 @@ pg_stat_get_backend_start(PG_FUNCTION_ARGS)
 	TimestampTz result;
 	PgBackendStatus *beentry;
 
-	if ((beentry = pgstat_fetch_stat_beentry(beid)) == NULL)
+	if ((beentry = pgstat_get_beentry_by_backend_id(beid)) == NULL)
 		PG_RETURN_NULL();
 
 	else if (!HAS_PGSTAT_PERMISSIONS(beentry->st_userid))
@@ -890,7 +890,7 @@ pg_stat_get_backend_client_addr(PG_FUNCTION_ARGS)
 	char		remote_host[NI_MAXHOST];
 	int			ret;
 
-	if ((beentry = pgstat_fetch_stat_beentry(beid)) == NULL)
+	if ((beentry = pgstat_get_beentry_by_backend_id(beid)) == NULL)
 		PG_RETURN_NULL();
 
 	else if (!HAS_PGSTAT_PERMISSIONS(beentry->st_userid))
@@ -935,7 +935,7 @@ pg_stat_get_backend_client_port(PG_FUNCTION_ARGS)
 	char		remote_port[NI_MAXSERV];
 	int			ret;
 
-	if ((beentry = pgstat_fetch_stat_beentry(beid)) == NULL)
+	if ((beentry = pgstat_get_beentry_by_backend_id(beid)) == NULL)
 		PG_RETURN_NULL();
 
 	else if (!HAS_PGSTAT_PERMISSIONS(beentry->st_userid))
@@ -978,12 +978,12 @@ pg_stat_get_db_numbackends(PG_FUNCTION_ARGS)
 	Oid			dbid = PG_GETARG_OID(0);
 	int32		result;
 	int			tot_backends = pgstat_fetch_stat_numbackends();
-	int			beid;
+	int			idx;
 
 	result = 0;
-	for (beid = 1; beid <= tot_backends; beid++)
+	for (idx = 1; idx <= tot_backends; idx++)
 	{
-		LocalPgBackendStatus *local_beentry = pgstat_fetch_stat_local_beentry(beid);
+		LocalPgBackendStatus *local_beentry = pgstat_get_local_beentry_by_index(idx);
 
 		if (local_beentry->backendStatus.st_databaseid == dbid)
 			result++;
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index 77939a0aed..1718ff7ce6 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -333,8 +333,8 @@ extern uint64 pgstat_get_my_query_id(void);
  * ----------
  */
 extern int	pgstat_fetch_stat_numbackends(void);
-extern PgBackendStatus *pgstat_fetch_stat_beentry(BackendId beid);
-extern LocalPgBackendStatus *pgstat_fetch_stat_local_beentry(int beid);
+extern PgBackendStatus *pgstat_get_beentry_by_backend_id(BackendId beid);
+extern LocalPgBackendStatus *pgstat_get_local_beentry_by_index(int idx);
 extern char *pgstat_clip_activity(const char *raw_activity);
 
 
-- 
2.25.1


--7AUc2qLy4jB3hD7Z
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v4-0002-fix-pg_stat_get_backend_subxact-to-use-real-backe.patch"



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-18 05:49  Andrey M. Borodin <[email protected]>
  parent: Andrey Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2023-12-18 05:49 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected]



> On 16 Dec 2023, at 05:58, Japin Li <[email protected]> wrote:
> 
> 
> On Fri, 15 Dec 2023 at 17:51, Andrey M. Borodin <[email protected]> wrote:
>>> On 8 Dec 2023, at 15:29, Japin Li <[email protected]> wrote:
>>> 
>>> Thanks for updating the patch. LGTM.
>> 
>> PFA v9. Changes:
>> 1. Added tests for idle_in_transaction_timeout
>> 2. Suppress statement_timeout if it’s shorter than transaction_timeout
>> 
> +       if (StatementTimeout > 0
> +               && IdleInTransactionSessionTimeout < TransactionTimeout)
>                   ^
> 
> Should be StatementTimeout?
Yes, that’s an oversight. I’ve adjusted tests so they catch this problem.

> Maybe we should add documentation to describe this behavior.

I've added a paragraph about it to config.sgml, but I'm not sure about the comprehensiveness of the wording.


Best regards, Andrey Borodin.


Attachments:

  [application/octet-stream] v10-0001-Introduce-transaction_timeout.patch (20.9K, ../../[email protected]/2-v10-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From d5f528c9ce74ae60b41f180d6afad7c8c2a38bb5 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v10] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>
Reviewed-by: Japin Li <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 35 ++++++++++++++
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 27 +++++++++--
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 ++++
 src/backend/utils/misc/guc_tables.c           | 11 +++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/Makefile                   |  5 +-
 src/test/isolation/expected/timeouts.out      | 47 ++++++++++++++++++-
 src/test/isolation/specs/timeouts.spec        | 30 +++++++++++-
 18 files changed, 173 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 94d1eb2b81..d62673051b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9075,6 +9075,41 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index b04fcfc8c8..e6fa1cfdc2 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b6451d9d08..4be06c1e5d 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7298a187d1..fd847e5a6c 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,6 +2745,10 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		/* Schedule or reschedule transaction timeout */
+		if (TransactionTimeout > 0)
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -3426,6 +3430,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,7 +4506,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& IdleInTransactionSessionTimeout < TransactionTimeout)
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4504,7 +4520,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& IdleInTransactionSessionTimeout < TransactionTimeout)
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4562,6 +4579,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5140,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& StatementTimeout < TransactionTimeout)
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 8e97a0150f..8f1157afee 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 60bc1217fb..fd586c193c 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 552cf9d950..64be4de0c7 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 6474e35ec0..4291a8cb01 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2544,6 +2544,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index cf9f283cfe..07ebec7709 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -695,6 +695,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4..3342971bd0 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8c0b5486b9..21bd16ef00 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1251,6 +1251,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 417c74cfef..9cda3f3667 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index f0cc651435..732ca7d0f6 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 4b25961249..a49a83607f 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -428,6 +428,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 8a61853371..608a83d5a8 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/Makefile b/src/test/isolation/Makefile
index ade2256ed3..2bd06f8f15 100644
--- a/src/test/isolation/Makefile
+++ b/src/test/isolation/Makefile
@@ -62,7 +62,7 @@ installcheck: all
 	$(pg_isolation_regress_installcheck) --schedule=$(srcdir)/isolation_schedule
 
 check: all
-	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule
+	$(pg_isolation_regress_check) timeouts
 
 # Non-default tests.  It only makes sense to run these if set up to use
 # prepared transactions, via TEMP_CONFIG for the check case, or via the
@@ -72,3 +72,6 @@ installcheck-prepared-txns: all temp-install
 
 check-prepared-txns: all temp-install
 	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule prepared-transactions prepared-transactions-cic
+
+check-timeouts: all temp-install
+	$(pg_isolation_regress_check) timeouts
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..c06fbaa67a 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 6 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,48 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 stt2_check itt4_set itt4_begin sleep_there stt3_check_itt4
+step stt1_set: SET transaction_timeout = '1ms';
+step stt1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_here: SELECT pg_sleep(0.01);
+FATAL:  terminating connection due to transaction timeout
+server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+
+step stt2_set: SET transaction_timeout = '1ms';
+step stt2_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.01);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
+count
+-----
+    0
+(1 row)
+
+step stt2_check: SELECT 1;
+FATAL:  terminating connection due to transaction timeout
+server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+
+step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
+step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.01);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
+step stt3_check_itt4: <... completed>
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..a67d59554e 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -18,7 +18,7 @@ step wrtbl	{ UPDATE accounts SET balance = balance + 100; }
 teardown	{ ABORT; }
 
 session s2
-setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+setup		{ SET transaction_timeout = '10s'; SET idle_in_transaction_session_timeout = '10s'; BEGIN ISOLATION LEVEL READ COMMITTED; }
 step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
@@ -27,6 +27,30 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session stt1
+# enable statement_timeout to check interaction
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt1_set	{ SET transaction_timeout = '1ms'; }
+step stt1_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step sleep_here	{ SELECT pg_sleep(0.01); }
+
+session stt2
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt2_set	{ SET transaction_timeout = '1ms'; }
+step stt2_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+# Session stt2 is terminated in the background. However, isolation tester needs a step to observe it.
+step stt2_check	{ SELECT 1; }
+
+session stt3
+step sleep_there{ SELECT pg_sleep(0.01); }
+# Observe that stt2 died
+step stt3_check_stt2 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2' }
+step stt3_check_itt4 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' }
+
+session itt4
+step itt4_set	{ SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s'; }
+step itt4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +71,7 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# timeout of active query, idle transaction timeout
+permutation stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 stt2_check itt4_set itt4_begin sleep_there stt3_check_itt4(*)
+# can't run tests after this, sessions stt1, stt2, and itt4 are expected to FATAL-out
-- 
2.42.0



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-18 09:32  Japin Li <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Japin Li @ 2023-12-18 09:32 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected]


On Mon, 18 Dec 2023 at 13:49, Andrey M. Borodin <[email protected]> wrote:
>> On 16 Dec 2023, at 05:58, Japin Li <[email protected]> wrote:
>>
>>
>> On Fri, 15 Dec 2023 at 17:51, Andrey M. Borodin <[email protected]> wrote:
>>>> On 8 Dec 2023, at 15:29, Japin Li <[email protected]> wrote:
>>>>
>>>> Thanks for updating the patch. LGTM.
>>>
>>> PFA v9. Changes:
>>> 1. Added tests for idle_in_transaction_timeout
>>> 2. Suppress statement_timeout if it’s shorter than transaction_timeout
>>>
>> +       if (StatementTimeout > 0
>> +               && IdleInTransactionSessionTimeout < TransactionTimeout)
>>                   ^
>>
>> Should be StatementTimeout?
> Yes, that’s an oversight. I’ve adjusted tests so they catch this problem.
>
>> Maybe we should add documentation to describe this behavior.
>
> I've added a paragraph about it to config.sgml, but I'm not sure about the comprehensiveness of the wording.
>

Thanks for updating the patch, no objections.

--
Regrads,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-18 09:40  Andrey M. Borodin <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2023-12-18 09:40 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected]



> On 18 Dec 2023, at 14:32, Japin Li <[email protected]> wrote:
> 
> 
> Thanks for updating the patch

Sorry for the noise, but commitfest bot found one more bug in handling statement timeout. PFA v11.


Best regards, Andrey Borodin.





Attachments:

  [application/octet-stream] v11-0001-Introduce-transaction_timeout.patch (20.9K, ../../[email protected]/2-v11-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From f9146d510cbf62191d3ae88202471a098464bcc0 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v11] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>
Reviewed-by: Japin Li <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 35 ++++++++++++++
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 27 +++++++++--
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 ++++
 src/backend/utils/misc/guc_tables.c           | 11 +++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/Makefile                   |  5 +-
 src/test/isolation/expected/timeouts.out      | 47 ++++++++++++++++++-
 src/test/isolation/specs/timeouts.spec        | 30 +++++++++++-
 18 files changed, 173 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 94d1eb2b81..d62673051b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9075,6 +9075,41 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index b04fcfc8c8..e6fa1cfdc2 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b6451d9d08..4be06c1e5d 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7298a187d1..a2611cf8e6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,6 +2745,10 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		/* Schedule or reschedule transaction timeout */
+		if (TransactionTimeout > 0)
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -3426,6 +3430,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,7 +4506,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4504,7 +4520,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4562,6 +4579,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5140,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 8e97a0150f..8f1157afee 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 60bc1217fb..fd586c193c 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 552cf9d950..64be4de0c7 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 6474e35ec0..4291a8cb01 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2544,6 +2544,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index cf9f283cfe..07ebec7709 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -695,6 +695,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4..3342971bd0 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8c0b5486b9..21bd16ef00 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1251,6 +1251,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 417c74cfef..9cda3f3667 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index f0cc651435..732ca7d0f6 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 4b25961249..a49a83607f 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -428,6 +428,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 8a61853371..608a83d5a8 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/Makefile b/src/test/isolation/Makefile
index ade2256ed3..2bd06f8f15 100644
--- a/src/test/isolation/Makefile
+++ b/src/test/isolation/Makefile
@@ -62,7 +62,7 @@ installcheck: all
 	$(pg_isolation_regress_installcheck) --schedule=$(srcdir)/isolation_schedule
 
 check: all
-	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule
+	$(pg_isolation_regress_check) timeouts
 
 # Non-default tests.  It only makes sense to run these if set up to use
 # prepared transactions, via TEMP_CONFIG for the check case, or via the
@@ -72,3 +72,6 @@ installcheck-prepared-txns: all temp-install
 
 check-prepared-txns: all temp-install
 	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule prepared-transactions prepared-transactions-cic
+
+check-timeouts: all temp-install
+	$(pg_isolation_regress_check) timeouts
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..c06fbaa67a 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 6 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,48 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 stt2_check itt4_set itt4_begin sleep_there stt3_check_itt4
+step stt1_set: SET transaction_timeout = '1ms';
+step stt1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_here: SELECT pg_sleep(0.01);
+FATAL:  terminating connection due to transaction timeout
+server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+
+step stt2_set: SET transaction_timeout = '1ms';
+step stt2_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.01);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
+count
+-----
+    0
+(1 row)
+
+step stt2_check: SELECT 1;
+FATAL:  terminating connection due to transaction timeout
+server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+
+step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
+step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.01);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
+step stt3_check_itt4: <... completed>
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..a67d59554e 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -18,7 +18,7 @@ step wrtbl	{ UPDATE accounts SET balance = balance + 100; }
 teardown	{ ABORT; }
 
 session s2
-setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+setup		{ SET transaction_timeout = '10s'; SET idle_in_transaction_session_timeout = '10s'; BEGIN ISOLATION LEVEL READ COMMITTED; }
 step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
@@ -27,6 +27,30 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session stt1
+# enable statement_timeout to check interaction
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt1_set	{ SET transaction_timeout = '1ms'; }
+step stt1_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step sleep_here	{ SELECT pg_sleep(0.01); }
+
+session stt2
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt2_set	{ SET transaction_timeout = '1ms'; }
+step stt2_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+# Session stt2 is terminated in the background. However, isolation tester needs a step to observe it.
+step stt2_check	{ SELECT 1; }
+
+session stt3
+step sleep_there{ SELECT pg_sleep(0.01); }
+# Observe that stt2 died
+step stt3_check_stt2 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2' }
+step stt3_check_itt4 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' }
+
+session itt4
+step itt4_set	{ SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s'; }
+step itt4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +71,7 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# timeout of active query, idle transaction timeout
+permutation stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 stt2_check itt4_set itt4_begin sleep_there stt3_check_itt4(*)
+# can't run tests after this, sessions stt1, stt2, and itt4 are expected to FATAL-out
-- 
2.42.0



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-19 01:25  Japin Li <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Japin Li @ 2023-12-19 01:25 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>


On Mon, 18 Dec 2023 at 17:40, Andrey M. Borodin <[email protected]> wrote:
>> On 18 Dec 2023, at 14:32, Japin Li <[email protected]> wrote:
>>
>>
>> Thanks for updating the patch
>
> Sorry for the noise, but commitfest bot found one more bug in handling statement timeout. PFA v11.
>

On Windows, there still have an error:

diff -w -U3 C:/cirrus/src/test/isolation/expected/timeouts.out C:/cirrus/build/testrun/isolation/isolation/results/timeouts.out
--- C:/cirrus/src/test/isolation/expected/timeouts.out	2023-12-18 10:22:21.772537200 +0000
+++ C:/cirrus/build/testrun/isolation/isolation/results/timeouts.out	2023-12-18 10:26:08.039831800 +0000
@@ -103,24 +103,7 @@
     0
 (1 row)

-step stt2_check: SELECT 1;
-FATAL:  terminating connection due to transaction timeout
-server closed the connection unexpectedly
+PQconsumeInput failed: server closed the connection unexpectedly
 	This probably means the server terminated abnormally
 	before or while processing the request.

-step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
-step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_there: SELECT pg_sleep(0.01);
-pg_sleep
---------
-
-(1 row)
-
-step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
-step stt3_check_itt4: <... completed>
-count
------
-    0
-(1 row)
-


--
Regrads,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-19 08:26  Andrey M. Borodin <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2023-12-19 08:26 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>



> On 19 Dec 2023, at 06:25, Japin Li <[email protected]> wrote:
> 
> On Windows, there still have an error:

Uhhmm, yes. Connection termination looks different on windows machine.
I’ve checked how this looks in relication slot tests and removed select that was observing connection failure.
I don’t have Windows machine, so I hope CF bot will pick this.


Best regards, Andrey Borodin.


Attachments:

  [application/octet-stream] v12-0001-Introduce-transaction_timeout.patch (20.7K, ../../[email protected]/2-v12-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From b7bde97801877d6e8c1fd16e58b4cf4e9d07ef1b Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v12] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>
Reviewed-by: Japin Li <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 35 ++++++++++++++++
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 27 ++++++++++--
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 +++++
 src/backend/utils/misc/guc_tables.c           | 11 +++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/Makefile                   |  5 ++-
 src/test/isolation/expected/timeouts.out      | 41 ++++++++++++++++++-
 src/test/isolation/specs/timeouts.spec        | 29 ++++++++++++-
 18 files changed, 166 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 94d1eb2b81..d62673051b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9075,6 +9075,41 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index b04fcfc8c8..e6fa1cfdc2 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b6451d9d08..4be06c1e5d 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7298a187d1..a2611cf8e6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,6 +2745,10 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		/* Schedule or reschedule transaction timeout */
+		if (TransactionTimeout > 0)
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -3426,6 +3430,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,7 +4506,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4504,7 +4520,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4562,6 +4579,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5140,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 8e97a0150f..8f1157afee 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 60bc1217fb..fd586c193c 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 552cf9d950..64be4de0c7 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 6474e35ec0..4291a8cb01 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2544,6 +2544,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index cf9f283cfe..07ebec7709 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -695,6 +695,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4..3342971bd0 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8c0b5486b9..21bd16ef00 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1251,6 +1251,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 417c74cfef..9cda3f3667 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index f0cc651435..732ca7d0f6 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 4b25961249..a49a83607f 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -428,6 +428,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 8a61853371..608a83d5a8 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/Makefile b/src/test/isolation/Makefile
index ade2256ed3..2bd06f8f15 100644
--- a/src/test/isolation/Makefile
+++ b/src/test/isolation/Makefile
@@ -62,7 +62,7 @@ installcheck: all
 	$(pg_isolation_regress_installcheck) --schedule=$(srcdir)/isolation_schedule
 
 check: all
-	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule
+	$(pg_isolation_regress_check) timeouts
 
 # Non-default tests.  It only makes sense to run these if set up to use
 # prepared transactions, via TEMP_CONFIG for the check case, or via the
@@ -72,3 +72,6 @@ installcheck-prepared-txns: all temp-install
 
 check-prepared-txns: all temp-install
 	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule prepared-transactions prepared-transactions-cic
+
+check-timeouts: all temp-install
+	$(pg_isolation_regress_check) timeouts
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..34f884bbba 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 6 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,42 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4
+step stt1_set: SET transaction_timeout = '1ms';
+step stt1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_here: SELECT pg_sleep(0.01);
+FATAL:  terminating connection due to transaction timeout
+server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+
+step stt2_set: SET transaction_timeout = '1ms';
+step stt2_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.01);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
+count
+-----
+    0
+(1 row)
+
+step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
+step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.01);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
+step stt3_check_itt4: <... completed>
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..7f62285923 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -18,7 +18,7 @@ step wrtbl	{ UPDATE accounts SET balance = balance + 100; }
 teardown	{ ABORT; }
 
 session s2
-setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+setup		{ SET transaction_timeout = '10s'; SET idle_in_transaction_session_timeout = '10s'; BEGIN ISOLATION LEVEL READ COMMITTED; }
 step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
@@ -27,6 +27,29 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session stt1
+# enable statement_timeout to check interaction
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt1_set	{ SET transaction_timeout = '1ms'; }
+step stt1_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step sleep_here	{ SELECT pg_sleep(0.01); }
+
+session stt2
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt2_set	{ SET transaction_timeout = '1ms'; }
+step stt2_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+# Session stt2 is terminated in the background. However, isolation tester needs a step to observe it.
+
+session stt3
+step sleep_there{ SELECT pg_sleep(0.01); }
+# Observe that stt2\itt4 died
+step stt3_check_stt2 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2' }
+step stt3_check_itt4 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' }
+
+session itt4
+step itt4_set	{ SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s'; }
+step itt4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +70,7 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# timeout of active query, idle transaction timeout
+permutation stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4(*)
+# can't run tests after this, sessions stt1, stt2, and itt4 are expected to FATAL-out
-- 
2.42.0



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-19 10:27  Andrey M. Borodin <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 2 replies; 76+ messages in thread

From: Andrey M. Borodin @ 2023-12-19 10:27 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>



> On 19 Dec 2023, at 13:26, Andrey M. Borodin <[email protected]> wrote:
> 
> I don’t have Windows machine, so I hope CF bot will pick this.

I used Github CI to produce version of tests that seems to be is stable on Windows.
Sorry for the noise.


Best regards, Andrey Borodin.


Attachments:

  [application/octet-stream] v13-0001-Introduce-transaction_timeout.patch (20.7K, ../../[email protected]/2-v13-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From 3dd79946578e66f335b9f4ae60e3a522a583a169 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v13] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>
Reviewed-by: Japin Li <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 35 ++++++++++++++++
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 27 ++++++++++--
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 +++++
 src/backend/utils/misc/guc_tables.c           | 11 +++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/Makefile                   |  5 ++-
 src/test/isolation/expected/timeouts.out      | 41 ++++++++++++++++++-
 src/test/isolation/specs/timeouts.spec        | 29 ++++++++++++-
 18 files changed, 166 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 94d1eb2b81..d62673051b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9075,6 +9075,41 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index b04fcfc8c8..e6fa1cfdc2 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b6451d9d08..4be06c1e5d 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7298a187d1..a2611cf8e6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,6 +2745,10 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		/* Schedule or reschedule transaction timeout */
+		if (TransactionTimeout > 0)
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -3426,6 +3430,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,7 +4506,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4504,7 +4520,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4562,6 +4579,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5140,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 8e97a0150f..8f1157afee 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 60bc1217fb..fd586c193c 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 552cf9d950..64be4de0c7 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 6474e35ec0..4291a8cb01 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2544,6 +2544,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index cf9f283cfe..07ebec7709 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -695,6 +695,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4..3342971bd0 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8c0b5486b9..21bd16ef00 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1251,6 +1251,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 417c74cfef..9cda3f3667 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index f0cc651435..732ca7d0f6 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 4b25961249..a49a83607f 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -428,6 +428,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 8a61853371..608a83d5a8 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/Makefile b/src/test/isolation/Makefile
index ade2256ed3..2bd06f8f15 100644
--- a/src/test/isolation/Makefile
+++ b/src/test/isolation/Makefile
@@ -62,7 +62,7 @@ installcheck: all
 	$(pg_isolation_regress_installcheck) --schedule=$(srcdir)/isolation_schedule
 
 check: all
-	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule
+	$(pg_isolation_regress_check) timeouts
 
 # Non-default tests.  It only makes sense to run these if set up to use
 # prepared transactions, via TEMP_CONFIG for the check case, or via the
@@ -72,3 +72,6 @@ installcheck-prepared-txns: all temp-install
 
 check-prepared-txns: all temp-install
 	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule prepared-transactions prepared-transactions-cic
+
+check-timeouts: all temp-install
+	$(pg_isolation_regress_check) timeouts
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..5072b9bfb7 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 6 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,42 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4
+step stt1_set: SET transaction_timeout = '1ms';
+step stt1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_here: SELECT pg_sleep(1);
+FATAL:  terminating connection due to transaction timeout
+server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+
+step stt2_set: SET transaction_timeout = '1ms';
+step stt2_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.01);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
+count
+-----
+    0
+(1 row)
+
+step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
+step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.01);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
+step stt3_check_itt4: <... completed>
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..136f83d62f 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -18,7 +18,7 @@ step wrtbl	{ UPDATE accounts SET balance = balance + 100; }
 teardown	{ ABORT; }
 
 session s2
-setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+setup		{ SET transaction_timeout = '10s'; SET idle_in_transaction_session_timeout = '10s'; BEGIN ISOLATION LEVEL READ COMMITTED; }
 step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
@@ -27,6 +27,29 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session stt1
+# enable statement_timeout to check interaction
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt1_set	{ SET transaction_timeout = '1ms'; }
+step stt1_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step sleep_here	{ SELECT pg_sleep(1); }
+
+session stt2
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt2_set	{ SET transaction_timeout = '1ms'; }
+step stt2_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+# Session stt2 is terminated in the background. However, isolation tester needs a step to observe it.
+
+session stt3
+step sleep_there{ SELECT pg_sleep(0.01); }
+# Observe that stt2\itt4 died
+step stt3_check_stt2 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2' }
+step stt3_check_itt4 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' }
+
+session itt4
+step itt4_set	{ SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s'; }
+step itt4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +70,7 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# timeout of active query, idle transaction timeout
+permutation stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4(*)
+# can't run tests after this, sessions stt1, stt2, and itt4 are expected to FATAL-out
-- 
2.42.0



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-19 14:06  Japin Li <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  1 sibling, 1 reply; 76+ messages in thread

From: Japin Li @ 2023-12-19 14:06 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>


On Tue, 19 Dec 2023 at 18:27, Andrey M. Borodin <[email protected]> wrote:
>> On 19 Dec 2023, at 13:26, Andrey M. Borodin <[email protected]> wrote:
>>
>> I don’t have Windows machine, so I hope CF bot will pick this.
>
> I used Github CI to produce version of tests that seems to be is stable on Windows.

It still failed on Windows Server 2019 [1].

diff -w -U3 C:/cirrus/src/test/isolation/expected/timeouts.out C:/cirrus/build/testrun/isolation/isolation/results/timeouts.out
--- C:/cirrus/src/test/isolation/expected/timeouts.out	2023-12-19 10:34:30.354721100 +0000
+++ C:/cirrus/build/testrun/isolation/isolation/results/timeouts.out	2023-12-19 10:38:25.877981600 +0000
@@ -100,7 +100,7 @@
 step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
 count
 -----
-    0
+    1
 (1 row)

 step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';

[1] https://api.cirrus-ci.com/v1/artifact/task/4707530400595968/testrun/build/testrun/isolation/isolatio...

--
Regrads,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-19 14:51  Junwang Zhao <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  1 sibling, 1 reply; 76+ messages in thread

From: Junwang Zhao @ 2023-12-19 14:51 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: Japin Li <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>

On Tue, Dec 19, 2023 at 6:27 PM Andrey M. Borodin <[email protected]> wrote:
>
>
>
> > On 19 Dec 2023, at 13:26, Andrey M. Borodin <[email protected]> wrote:
> >
> > I don’t have Windows machine, so I hope CF bot will pick this.
>
> I used Github CI to produce version of tests that seems to be is stable on Windows.
> Sorry for the noise.
>
>
> Best regards, Andrey Borodin.

+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or
<varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>

When transaction_timeout is *equal* to idle_in_transaction_session_timeout
or statement_timeout, idle_in_transaction_session_timeout and statement_timeout
will also be invalidated, the logic in the code seems right, though
this document
is a little bit inaccurate.

-- 
Regards
Junwang Zhao






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-20 01:48  Junwang Zhao <[email protected]>
  parent: Junwang Zhao <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Junwang Zhao @ 2023-12-20 01:48 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: Japin Li <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>

On Tue, Dec 19, 2023 at 10:51 PM Junwang Zhao <[email protected]> wrote:
>
> On Tue, Dec 19, 2023 at 6:27 PM Andrey M. Borodin <[email protected]> wrote:
> >
> >
> >
> > > On 19 Dec 2023, at 13:26, Andrey M. Borodin <[email protected]> wrote:
> > >
> > > I don’t have Windows machine, so I hope CF bot will pick this.
> >
> > I used Github CI to produce version of tests that seems to be is stable on Windows.
> > Sorry for the noise.
> >
> >
> > Best regards, Andrey Borodin.
>
> +       <para>
> +        If <varname>transaction_timeout</varname> is shorter than
> +        <varname>idle_in_transaction_session_timeout</varname> or
> <varname>statement_timeout</varname>
> +        <varname>transaction_timeout</varname> will invalidate longer timeout.
> +       </para>
>
> When transaction_timeout is *equal* to idle_in_transaction_session_timeout
> or statement_timeout, idle_in_transaction_session_timeout and statement_timeout
> will also be invalidated, the logic in the code seems right, though
> this document
> is a little bit inaccurate.
>
       <para>
        Unlike <varname>statement_timeout</varname>, this timeout can only occur
        while waiting for locks.  Note that if
<varname>statement_timeout</varname>
        is nonzero, it is rather pointless to set
<varname>lock_timeout</varname> to
        the same or larger value, since the statement timeout would always
        trigger first.  If <varname>log_min_error_statement</varname> is set to
        <literal>ERROR</literal> or lower, the statement that timed out will be
        logged.
       </para>

There is a note about statement_timeout and lock_timeout, set both
and lock_timeout >= statement_timeout is pointless, but this logic seems not
implemented in the code. I am wondering if lock_timeout >= transaction_timeout,
should we invalidate lock_timeout? Or maybe just document this.

> --
> Regards
> Junwang Zhao



-- 
Regards
Junwang Zhao






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* 回复: Transaction timeout
@ 2023-12-20 01:58  Thomas wen <[email protected]>
  parent: Junwang Zhao <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Thomas wen @ 2023-12-20 01:58 UTC (permalink / raw)
  To: Junwang Zhao <[email protected]>; Andrey M. Borodin <[email protected]>; +Cc: Japin Li <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>

Hi Junwang Zhao
     #should we invalidate lock_timeout? Or maybe just document this.
I think you mean when lock_time is greater than trasaction-time invalidate lock_timeout or  needs to be logged ?




Best whish
________________________________
发件人: Junwang Zhao <[email protected]>
发送时间: 2023年12月20日 9:48
收件人: Andrey M. Borodin <[email protected]>
抄送: Japin Li <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers <[email protected]>; [email protected] <[email protected]>
主题: Re: Transaction timeout

On Tue, Dec 19, 2023 at 10:51 PM Junwang Zhao <[email protected]> wrote:
>
> On Tue, Dec 19, 2023 at 6:27 PM Andrey M. Borodin <[email protected]> wrote:
> >
> >
> >
> > > On 19 Dec 2023, at 13:26, Andrey M. Borodin <[email protected]> wrote:
> > >
> > > I don’t have Windows machine, so I hope CF bot will pick this.
> >
> > I used Github CI to produce version of tests that seems to be is stable on Windows.
> > Sorry for the noise.
> >
> >
> > Best regards, Andrey Borodin.
>
> +       <para>
> +        If <varname>transaction_timeout</varname> is shorter than
> +        <varname>idle_in_transaction_session_timeout</varname> or
> <varname>statement_timeout</varname>
> +        <varname>transaction_timeout</varname> will invalidate longer timeout.
> +       </para>
>
> When transaction_timeout is *equal* to idle_in_transaction_session_timeout
> or statement_timeout, idle_in_transaction_session_timeout and statement_timeout
> will also be invalidated, the logic in the code seems right, though
> this document
> is a little bit inaccurate.
>
       <para>
        Unlike <varname>statement_timeout</varname>, this timeout can only occur
        while waiting for locks.  Note that if
<varname>statement_timeout</varname>
        is nonzero, it is rather pointless to set
<varname>lock_timeout</varname> to
        the same or larger value, since the statement timeout would always
        trigger first.  If <varname>log_min_error_statement</varname> is set to
        <literal>ERROR</literal> or lower, the statement that timed out will be
        logged.
       </para>

There is a note about statement_timeout and lock_timeout, set both
and lock_timeout >= statement_timeout is pointless, but this logic seems not
implemented in the code. I am wondering if lock_timeout >= transaction_timeout,
should we invalidate lock_timeout? Or maybe just document this.

> --
> Regards
> Junwang Zhao



--
Regards
Junwang Zhao




^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-20 02:35  Junwang Zhao <[email protected]>
  parent: Thomas wen <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Junwang Zhao @ 2023-12-20 02:35 UTC (permalink / raw)
  To: Thomas wen <[email protected]>; +Cc: Andrey M. Borodin <[email protected]>; Japin Li <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>

On Wed, Dec 20, 2023 at 9:58 AM Thomas wen
<[email protected]> wrote:
>
> Hi Junwang Zhao
>      #should we invalidate lock_timeout? Or maybe just document this.
> I think you mean when lock_time is greater than trasaction-time invalidate lock_timeout or  needs to be logged ?
>
I mean the interleaving of the gucs, which is lock_timeout and the new
introduced transaction_timeout,
if lock_timeout >= transaction_timeout, seems no need to enable lock_timeout.
>
>
>
> Best whish
> ________________________________
> 发件人: Junwang Zhao <[email protected]>
> 发送时间: 2023年12月20日 9:48
> 收件人: Andrey M. Borodin <[email protected]>
> 抄送: Japin Li <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers <[email protected]>; [email protected] <[email protected]>
> 主题: Re: Transaction timeout
>
> On Tue, Dec 19, 2023 at 10:51 PM Junwang Zhao <[email protected]> wrote:
> >
> > On Tue, Dec 19, 2023 at 6:27 PM Andrey M. Borodin <[email protected]> wrote:
> > >
> > >
> > >
> > > > On 19 Dec 2023, at 13:26, Andrey M. Borodin <[email protected]> wrote:
> > > >
> > > > I don’t have Windows machine, so I hope CF bot will pick this.
> > >
> > > I used Github CI to produce version of tests that seems to be is stable on Windows.
> > > Sorry for the noise.
> > >
> > >
> > > Best regards, Andrey Borodin.
> >
> > +       <para>
> > +        If <varname>transaction_timeout</varname> is shorter than
> > +        <varname>idle_in_transaction_session_timeout</varname> or
> > <varname>statement_timeout</varname>
> > +        <varname>transaction_timeout</varname> will invalidate longer timeout.
> > +       </para>
> >
> > When transaction_timeout is *equal* to idle_in_transaction_session_timeout
> > or statement_timeout, idle_in_transaction_session_timeout and statement_timeout
> > will also be invalidated, the logic in the code seems right, though
> > this document
> > is a little bit inaccurate.
> >
>        <para>
>         Unlike <varname>statement_timeout</varname>, this timeout can only occur
>         while waiting for locks.  Note that if
> <varname>statement_timeout</varname>
>         is nonzero, it is rather pointless to set
> <varname>lock_timeout</varname> to
>         the same or larger value, since the statement timeout would always
>         trigger first.  If <varname>log_min_error_statement</varname> is set to
>         <literal>ERROR</literal> or lower, the statement that timed out will be
>         logged.
>        </para>
>
> There is a note about statement_timeout and lock_timeout, set both
> and lock_timeout >= statement_timeout is pointless, but this logic seems not
> implemented in the code. I am wondering if lock_timeout >= transaction_timeout,
> should we invalidate lock_timeout? Or maybe just document this.
>
> > --
> > Regards
> > Junwang Zhao
>
>
>
> --
> Regards
> Junwang Zhao
>
>


-- 
Regards
Junwang Zhao






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-20 12:23  wenhui qiu <[email protected]>
  parent: Junwang Zhao <[email protected]>
  0 siblings, 0 replies; 76+ messages in thread

From: wenhui qiu @ 2023-12-20 12:23 UTC (permalink / raw)
  To: Junwang Zhao <[email protected]>; +Cc: Thomas wen <[email protected]>; Andrey M. Borodin <[email protected]>; Japin Li <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>

Hi Junwang Zhao
   Agree +1

Best whish

Junwang Zhao <[email protected]> 于2023年12月20日周三 10:35写道:

> On Wed, Dec 20, 2023 at 9:58 AM Thomas wen
> <[email protected]> wrote:
> >
> > Hi Junwang Zhao
> >      #should we invalidate lock_timeout? Or maybe just document this.
> > I think you mean when lock_time is greater than trasaction-time
> invalidate lock_timeout or  needs to be logged ?
> >
> I mean the interleaving of the gucs, which is lock_timeout and the new
> introduced transaction_timeout,
> if lock_timeout >= transaction_timeout, seems no need to enable
> lock_timeout.
> >
> >
> >
> > Best whish
> > ________________________________
> > 发件人: Junwang Zhao <[email protected]>
> > 发送时间: 2023年12月20日 9:48
> > 收件人: Andrey M. Borodin <[email protected]>
> > 抄送: Japin Li <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao
> <[email protected]>; Andrey Borodin <[email protected]>;
> Andres Freund <[email protected]>; Michael Paquier <
> [email protected]>; Nikolay Samokhvalov <[email protected]>;
> pgsql-hackers <[email protected]>;
> [email protected] <[email protected]>
> > 主题: Re: Transaction timeout
> >
> > On Tue, Dec 19, 2023 at 10:51 PM Junwang Zhao <[email protected]> wrote:
> > >
> > > On Tue, Dec 19, 2023 at 6:27 PM Andrey M. Borodin <
> [email protected]> wrote:
> > > >
> > > >
> > > >
> > > > > On 19 Dec 2023, at 13:26, Andrey M. Borodin <[email protected]>
> wrote:
> > > > >
> > > > > I don’t have Windows machine, so I hope CF bot will pick this.
> > > >
> > > > I used Github CI to produce version of tests that seems to be is
> stable on Windows.
> > > > Sorry for the noise.
> > > >
> > > >
> > > > Best regards, Andrey Borodin.
> > >
> > > +       <para>
> > > +        If <varname>transaction_timeout</varname> is shorter than
> > > +        <varname>idle_in_transaction_session_timeout</varname> or
> > > <varname>statement_timeout</varname>
> > > +        <varname>transaction_timeout</varname> will invalidate longer
> timeout.
> > > +       </para>
> > >
> > > When transaction_timeout is *equal* to
> idle_in_transaction_session_timeout
> > > or statement_timeout, idle_in_transaction_session_timeout and
> statement_timeout
> > > will also be invalidated, the logic in the code seems right, though
> > > this document
> > > is a little bit inaccurate.
> > >
> >        <para>
> >         Unlike <varname>statement_timeout</varname>, this timeout can
> only occur
> >         while waiting for locks.  Note that if
> > <varname>statement_timeout</varname>
> >         is nonzero, it is rather pointless to set
> > <varname>lock_timeout</varname> to
> >         the same or larger value, since the statement timeout would
> always
> >         trigger first.  If <varname>log_min_error_statement</varname> is
> set to
> >         <literal>ERROR</literal> or lower, the statement that timed out
> will be
> >         logged.
> >        </para>
> >
> > There is a note about statement_timeout and lock_timeout, set both
> > and lock_timeout >= statement_timeout is pointless, but this logic seems
> not
> > implemented in the code. I am wondering if lock_timeout >=
> transaction_timeout,
> > should we invalidate lock_timeout? Or maybe just document this.
> >
> > > --
> > > Regards
> > > Junwang Zhao
> >
> >
> >
> > --
> > Regards
> > Junwang Zhao
> >
> >
>
>
> --
> Regards
> Junwang Zhao
>
>
>


^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-22 05:39  Japin Li <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Japin Li @ 2023-12-22 05:39 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: Andrey M. Borodin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected]


On Tue, 19 Dec 2023 at 22:06, Japin Li <[email protected]> wrote:
> On Tue, 19 Dec 2023 at 18:27, Andrey M. Borodin <[email protected]> wrote:
>>> On 19 Dec 2023, at 13:26, Andrey M. Borodin <[email protected]> wrote:
>>>
>>> I don’t have Windows machine, so I hope CF bot will pick this.
>>
>> I used Github CI to produce version of tests that seems to be is stable on Windows.
>
> It still failed on Windows Server 2019 [1].
>
> diff -w -U3 C:/cirrus/src/test/isolation/expected/timeouts.out C:/cirrus/build/testrun/isolation/isolation/results/timeouts.out
> --- C:/cirrus/src/test/isolation/expected/timeouts.out	2023-12-19 10:34:30.354721100 +0000
> +++ C:/cirrus/build/testrun/isolation/isolation/results/timeouts.out	2023-12-19 10:38:25.877981600 +0000
> @@ -100,7 +100,7 @@
>  step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
>  count
>  -----
> -    0
> +    1
>  (1 row)
>
>  step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
>
> [1] https://api.cirrus-ci.com/v1/artifact/task/4707530400595968/testrun/build/testrun/isolation/isolatio...

Hi,

I try to split the test for transaction timeout, and all passed on my CI [1].

OTOH, I find if I set transaction_timeout in a transaction, it will not take
effect immediately.  For example:

[local]:2049802 postgres=# BEGIN;
BEGIN
[local]:2049802 postgres=*# SET transaction_timeout TO '1s';
SET
[local]:2049802 postgres=*# SELECT relname FROM pg_class LIMIT 1;  -- wait 10s
   relname
--------------
 pg_statistic
(1 row)

[local]:2049802 postgres=*# SELECT relname FROM pg_class LIMIT 1;
FATAL:  terminating connection due to transaction timeout
server closed the connection unexpectedly
        This probably means the server terminated abnormally
        before or while processing the request.
The connection to the server was lost. Attempting reset: Succeeded.

It looks odd.  Does this is expected? I'm not read all the threads,
am I missing something?

[1] https://cirrus-ci.com/build/6574686130143232

--
Regrads,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.



Attachments:

  [text/x-diff] v14-0001-Introduce-transaction_timeout.patch (20.6K, ../../ME3P282MB3166FCD27CCC06C8E94134B5B694A@ME3P282MB3166.AUSP282.PROD.OUTLOOK.COM/2-v14-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From fb87e5fe2ea5ced51a7e443243cdd40115423449 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v13 1/1] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>
Reviewed-by: Japin Li <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 35 +++++++++++
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 27 +++++++-
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 +++
 src/backend/utils/misc/guc_tables.c           | 11 ++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/Makefile                   |  5 +-
 src/test/isolation/expected/timeouts.out      | 63 ++++++++++++++++++-
 src/test/isolation/specs/timeouts.spec        | 30 +++++++++
 18 files changed, 190 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index b5624ca884..d62edcf83b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9134,6 +9134,41 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index b04fcfc8c8..e6fa1cfdc2 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b6451d9d08..4be06c1e5d 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7298a187d1..a2611cf8e6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,6 +2745,10 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		/* Schedule or reschedule transaction timeout */
+		if (TransactionTimeout > 0)
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -3426,6 +3430,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,7 +4506,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4504,7 +4520,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4562,6 +4579,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5140,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 8e97a0150f..8f1157afee 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 60bc1217fb..fd586c193c 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 552cf9d950..64be4de0c7 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 9f59440526..c5cfbef02b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2556,6 +2556,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index b2809c711a..0b37117eb7 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -700,6 +700,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4..3342971bd0 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8c0b5486b9..21bd16ef00 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1251,6 +1251,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 417c74cfef..9cda3f3667 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 74bc2f97cb..b2d0f84252 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index e87fd25d64..9dde9cbfdd 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -429,6 +429,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 8a61853371..608a83d5a8 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/Makefile b/src/test/isolation/Makefile
index ade2256ed3..2bd06f8f15 100644
--- a/src/test/isolation/Makefile
+++ b/src/test/isolation/Makefile
@@ -62,7 +62,7 @@ installcheck: all
 	$(pg_isolation_regress_installcheck) --schedule=$(srcdir)/isolation_schedule
 
 check: all
-	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule
+	$(pg_isolation_regress_check) timeouts
 
 # Non-default tests.  It only makes sense to run these if set up to use
 # prepared transactions, via TEMP_CONFIG for the check case, or via the
@@ -72,3 +72,6 @@ installcheck-prepared-txns: all temp-install
 
 check-prepared-txns: all temp-install
 	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule prepared-transactions prepared-transactions-cic
+
+check-timeouts: all temp-install
+	$(pg_isolation_regress_check) timeouts
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..5b06148cee 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 6 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,64 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stto s3_begin sleep s3_check abort
+step stto: SET statement_timeout = '1ms'; SET transaction_timeout = '1s';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep: SELECT pg_sleep(0.01);
+ERROR:  canceling statement due to statement timeout
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    1
+(1 row)
+
+step abort: ABORT;
+
+starting permutation: tsto s3_begin wait_check s3_check
+step tsto: SET statement_timeout = '1s'; SET transaction_timeout = '1ms';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step wait_check: SELECT pg_sleep(0.01);
+pg_sleep
+--------
+        
+(1 row)
+
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: itto s4_begin wait_check s4_check
+step itto: SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s';
+step s4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step wait_check: SELECT pg_sleep(0.01);
+pg_sleep
+--------
+        
+(1 row)
+
+step s4_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: tito s5_begin wait_check s5_check
+step tito: SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms';
+step s5_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step wait_check: SELECT pg_sleep(0.01);
+pg_sleep
+--------
+        
+(1 row)
+
+step s5_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5';
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..0cca6ff147 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -27,6 +27,27 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session s3
+step s3_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step stto	{ SET statement_timeout = '1ms'; SET transaction_timeout = '1s'; }
+step tsto	{ SET statement_timeout = '1s'; SET transaction_timeout = '1ms'; }
+step sleep	{ SELECT pg_sleep(0.01); }
+step abort	{ ABORT; }
+
+session s4
+step s4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step itto	{ SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s'; }
+
+session s5
+step s5_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step tito	{ SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms'; }
+
+session s6
+step wait_check	{ SELECT pg_sleep(0.01); }
+step s3_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3'; }
+step s4_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4'; }
+step s5_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5'; }
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +68,12 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# statement timeout expires first
+permutation stto s3_begin sleep s3_check abort
+# transaction timeout expires first, session s3 FATAL-out
+permutation tsto s3_begin wait_check s3_check
+# idle in transaction timeout expires first, session s4 FATAL-out
+permutation itto s4_begin wait_check s4_check
+# transaction timeout expires first, session s5 FATAL-out
+permutation tito s5_begin wait_check s5_check
-- 
2.34.1



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-22 12:29  Junwang Zhao <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Junwang Zhao @ 2023-12-22 12:29 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: Andrey M. Borodin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected]

On Fri, Dec 22, 2023 at 1:39 PM Japin Li <[email protected]> wrote:
>
>
> On Tue, 19 Dec 2023 at 22:06, Japin Li <[email protected]> wrote:
> > On Tue, 19 Dec 2023 at 18:27, Andrey M. Borodin <[email protected]> wrote:
> >>> On 19 Dec 2023, at 13:26, Andrey M. Borodin <[email protected]> wrote:
> >>>
> >>> I don’t have Windows machine, so I hope CF bot will pick this.
> >>
> >> I used Github CI to produce version of tests that seems to be is stable on Windows.
> >
> > It still failed on Windows Server 2019 [1].
> >
> > diff -w -U3 C:/cirrus/src/test/isolation/expected/timeouts.out C:/cirrus/build/testrun/isolation/isolation/results/timeouts.out
> > --- C:/cirrus/src/test/isolation/expected/timeouts.out        2023-12-19 10:34:30.354721100 +0000
> > +++ C:/cirrus/build/testrun/isolation/isolation/results/timeouts.out  2023-12-19 10:38:25.877981600 +0000
> > @@ -100,7 +100,7 @@
> >  step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
> >  count
> >  -----
> > -    0
> > +    1
> >  (1 row)
> >
> >  step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
> >
> > [1] https://api.cirrus-ci.com/v1/artifact/task/4707530400595968/testrun/build/testrun/isolation/isolatio...
>
> Hi,
>
> I try to split the test for transaction timeout, and all passed on my CI [1].
>
> OTOH, I find if I set transaction_timeout in a transaction, it will not take
> effect immediately.  For example:
>
> [local]:2049802 postgres=# BEGIN;
> BEGIN
> [local]:2049802 postgres=*# SET transaction_timeout TO '1s';
when this execute, TransactionTimeout is still 0, this command will
not set timeout
> SET
> [local]:2049802 postgres=*# SELECT relname FROM pg_class LIMIT 1;  -- wait 10s
when this command get execute, start_xact_command will enable the timer
>    relname
> --------------
>  pg_statistic
> (1 row)
>
> [local]:2049802 postgres=*# SELECT relname FROM pg_class LIMIT 1;
> FATAL:  terminating connection due to transaction timeout
> server closed the connection unexpectedly
>         This probably means the server terminated abnormally
>         before or while processing the request.
> The connection to the server was lost. Attempting reset: Succeeded.
>
> It looks odd.  Does this is expected? I'm not read all the threads,
> am I missing something?

I think this is by design, if you debug statement_timeout, it's the same
behaviour, the timeout will be set for each command after the second
command was called, you just aren't aware of this.

I doubt people will set this in a transaction.
>
> [1] https://cirrus-ci.com/build/6574686130143232
>
> --
> Regrads,
> Japin Li
> ChengDu WenWu Information Technology Co., Ltd.
>


-- 
Regards
Junwang Zhao






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-22 14:25  Japin Li <[email protected]>
  parent: Junwang Zhao <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Japin Li @ 2023-12-22 14:25 UTC (permalink / raw)
  To: Junwang Zhao <[email protected]>; +Cc: Andrey M. Borodin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected]


On Fri, 22 Dec 2023 at 20:29, Junwang Zhao <[email protected]> wrote:
> On Fri, Dec 22, 2023 at 1:39 PM Japin Li <[email protected]> wrote:
>>
>>
>> On Tue, 19 Dec 2023 at 22:06, Japin Li <[email protected]> wrote:
>> > On Tue, 19 Dec 2023 at 18:27, Andrey M. Borodin <[email protected]> wrote:
>> >>> On 19 Dec 2023, at 13:26, Andrey M. Borodin <[email protected]> wrote:
>> >>>
>> >>> I don’t have Windows machine, so I hope CF bot will pick this.
>> >>
>> >> I used Github CI to produce version of tests that seems to be is stable on Windows.
>> >
>> > It still failed on Windows Server 2019 [1].
>> >
>> > diff -w -U3 C:/cirrus/src/test/isolation/expected/timeouts.out C:/cirrus/build/testrun/isolation/isolation/results/timeouts.out
>> > --- C:/cirrus/src/test/isolation/expected/timeouts.out        2023-12-19 10:34:30.354721100 +0000
>> > +++ C:/cirrus/build/testrun/isolation/isolation/results/timeouts.out  2023-12-19 10:38:25.877981600 +0000
>> > @@ -100,7 +100,7 @@
>> >  step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
>> >  count
>> >  -----
>> > -    0
>> > +    1
>> >  (1 row)
>> >
>> >  step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
>> >
>> > [1] https://api.cirrus-ci.com/v1/artifact/task/4707530400595968/testrun/build/testrun/isolation/isolatio...
>>
>> Hi,
>>
>> I try to split the test for transaction timeout, and all passed on my CI [1].
>>
>> OTOH, I find if I set transaction_timeout in a transaction, it will not take
>> effect immediately.  For example:
>>
>> [local]:2049802 postgres=# BEGIN;
>> BEGIN
>> [local]:2049802 postgres=*# SET transaction_timeout TO '1s';
> when this execute, TransactionTimeout is still 0, this command will
> not set timeout
>> SET
>> [local]:2049802 postgres=*# SELECT relname FROM pg_class LIMIT 1;  -- wait 10s
> when this command get execute, start_xact_command will enable the timer

Thanks for your exaplantion, got it.

>>    relname
>> --------------
>>  pg_statistic
>> (1 row)
>>
>> [local]:2049802 postgres=*# SELECT relname FROM pg_class LIMIT 1;
>> FATAL:  terminating connection due to transaction timeout
>> server closed the connection unexpectedly
>>         This probably means the server terminated abnormally
>>         before or while processing the request.
>> The connection to the server was lost. Attempting reset: Succeeded.
>>
>> It looks odd.  Does this is expected? I'm not read all the threads,
>> am I missing something?
>
> I think this is by design, if you debug statement_timeout, it's the same
> behaviour, the timeout will be set for each command after the second
> command was called, you just aren't aware of this.
>

I try to set idle_in_transaction_session_timeout after begin transaction,
it changes immediately, so I think transaction_timeout should also be take
immediately.

> I doubt people will set this in a transaction.

Maybe not,


--
Regrads,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-22 14:37  Junwang Zhao <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Junwang Zhao @ 2023-12-22 14:37 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: Andrey M. Borodin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected]

On Fri, Dec 22, 2023 at 10:25 PM Japin Li <[email protected]> wrote:
>
>
> On Fri, 22 Dec 2023 at 20:29, Junwang Zhao <[email protected]> wrote:
> > On Fri, Dec 22, 2023 at 1:39 PM Japin Li <[email protected]> wrote:
> >>
> >>
> >> On Tue, 19 Dec 2023 at 22:06, Japin Li <[email protected]> wrote:
> >> > On Tue, 19 Dec 2023 at 18:27, Andrey M. Borodin <[email protected]> wrote:
> >> >>> On 19 Dec 2023, at 13:26, Andrey M. Borodin <[email protected]> wrote:
> >> >>>
> >> >>> I don’t have Windows machine, so I hope CF bot will pick this.
> >> >>
> >> >> I used Github CI to produce version of tests that seems to be is stable on Windows.
> >> >
> >> > It still failed on Windows Server 2019 [1].
> >> >
> >> > diff -w -U3 C:/cirrus/src/test/isolation/expected/timeouts.out C:/cirrus/build/testrun/isolation/isolation/results/timeouts.out
> >> > --- C:/cirrus/src/test/isolation/expected/timeouts.out        2023-12-19 10:34:30.354721100 +0000
> >> > +++ C:/cirrus/build/testrun/isolation/isolation/results/timeouts.out  2023-12-19 10:38:25.877981600 +0000
> >> > @@ -100,7 +100,7 @@
> >> >  step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
> >> >  count
> >> >  -----
> >> > -    0
> >> > +    1
> >> >  (1 row)
> >> >
> >> >  step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
> >> >
> >> > [1] https://api.cirrus-ci.com/v1/artifact/task/4707530400595968/testrun/build/testrun/isolation/isolatio...
> >>
> >> Hi,
> >>
> >> I try to split the test for transaction timeout, and all passed on my CI [1].
> >>
> >> OTOH, I find if I set transaction_timeout in a transaction, it will not take
> >> effect immediately.  For example:
> >>
> >> [local]:2049802 postgres=# BEGIN;
> >> BEGIN
> >> [local]:2049802 postgres=*# SET transaction_timeout TO '1s';
> > when this execute, TransactionTimeout is still 0, this command will
> > not set timeout
> >> SET
> >> [local]:2049802 postgres=*# SELECT relname FROM pg_class LIMIT 1;  -- wait 10s
> > when this command get execute, start_xact_command will enable the timer
>
> Thanks for your exaplantion, got it.
>
> >>    relname
> >> --------------
> >>  pg_statistic
> >> (1 row)
> >>
> >> [local]:2049802 postgres=*# SELECT relname FROM pg_class LIMIT 1;
> >> FATAL:  terminating connection due to transaction timeout
> >> server closed the connection unexpectedly
> >>         This probably means the server terminated abnormally
> >>         before or while processing the request.
> >> The connection to the server was lost. Attempting reset: Succeeded.
> >>
> >> It looks odd.  Does this is expected? I'm not read all the threads,
> >> am I missing something?
> >
> > I think this is by design, if you debug statement_timeout, it's the same
> > behaviour, the timeout will be set for each command after the second
> > command was called, you just aren't aware of this.
> >
>
> I try to set idle_in_transaction_session_timeout after begin transaction,
> it changes immediately, so I think transaction_timeout should also be take
> immediately.

Ah, right, idle_in_transaction_session_timeout is set after the set
command finishes and before the backend send *ready for query*
to the client, so the value of the GUC is already set before
next command.

I bet you must have checked this ;)

>
> > I doubt people will set this in a transaction.
>
> Maybe not,
>
>
> --
> Regrads,
> Japin Li
> ChengDu WenWu Information Technology Co., Ltd.



-- 
Regards
Junwang Zhao






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-22 14:44  Japin Li <[email protected]>
  parent: Junwang Zhao <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Japin Li @ 2023-12-22 14:44 UTC (permalink / raw)
  To: Junwang Zhao <[email protected]>; +Cc: Andrey M. Borodin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected]


On Fri, 22 Dec 2023 at 22:37, Junwang Zhao <[email protected]> wrote:
> On Fri, Dec 22, 2023 at 10:25 PM Japin Li <[email protected]> wrote:
>> I try to set idle_in_transaction_session_timeout after begin transaction,
>> it changes immediately, so I think transaction_timeout should also be take
>> immediately.
>
> Ah, right, idle_in_transaction_session_timeout is set after the set
> command finishes and before the backend send *ready for query*
> to the client, so the value of the GUC is already set before
> next command.
>

I mean, is it possible to set transaction_timeout before next comand?


--
Regrads,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-22 15:30  Junwang Zhao <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Junwang Zhao @ 2023-12-22 15:30 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: Andrey M. Borodin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected]

On Fri, Dec 22, 2023 at 10:44 PM Japin Li <[email protected]> wrote:
>
>
> On Fri, 22 Dec 2023 at 22:37, Junwang Zhao <[email protected]> wrote:
> > On Fri, Dec 22, 2023 at 10:25 PM Japin Li <[email protected]> wrote:
> >> I try to set idle_in_transaction_session_timeout after begin transaction,
> >> it changes immediately, so I think transaction_timeout should also be take
> >> immediately.
> >
> > Ah, right, idle_in_transaction_session_timeout is set after the set
> > command finishes and before the backend send *ready for query*
> > to the client, so the value of the GUC is already set before
> > next command.
> >
>
> I mean, is it possible to set transaction_timeout before next comand?
>
Yeah, it's possible, set transaction_timeout in the when it first
goes into *idle in transaction* mode, see the attached files.

>
> --
> Regrads,
> Japin Li
> ChengDu WenWu Information Technology Co., Ltd.



-- 
Regards
Junwang Zhao


Attachments:

  [application/octet-stream] v15-0001-Introduce-transaction_timeout.patch (20.6K, ../../CAEG8a3K8kD0mQDFzj=v0zV7h+m4J4TDBLkXSJJS3+VGWNvnDCQ@mail.gmail.com/2-v15-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From 56091ac853d426464a8db948592f03fdd3a76289 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v15 1/2] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>
Reviewed-by: Japin Li <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 35 +++++++++++
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 27 +++++++-
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 +++
 src/backend/utils/misc/guc_tables.c           | 11 ++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/Makefile                   |  5 +-
 src/test/isolation/expected/timeouts.out      | 63 ++++++++++++++++++-
 src/test/isolation/specs/timeouts.spec        | 30 +++++++++
 18 files changed, 190 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index b5624ca884..d62edcf83b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9134,6 +9134,41 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index b04fcfc8c8..e6fa1cfdc2 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b6451d9d08..4be06c1e5d 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7298a187d1..a2611cf8e6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,6 +2745,10 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		/* Schedule or reschedule transaction timeout */
+		if (TransactionTimeout > 0)
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -3426,6 +3430,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,7 +4506,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4504,7 +4520,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4562,6 +4579,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5140,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 8e97a0150f..8f1157afee 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 60bc1217fb..fd586c193c 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 552cf9d950..64be4de0c7 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 9f59440526..c5cfbef02b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2556,6 +2556,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index b2809c711a..0b37117eb7 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -700,6 +700,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4..3342971bd0 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8c0b5486b9..21bd16ef00 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1251,6 +1251,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 417c74cfef..9cda3f3667 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 74bc2f97cb..b2d0f84252 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index e87fd25d64..9dde9cbfdd 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -429,6 +429,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 8a61853371..608a83d5a8 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/Makefile b/src/test/isolation/Makefile
index ade2256ed3..2bd06f8f15 100644
--- a/src/test/isolation/Makefile
+++ b/src/test/isolation/Makefile
@@ -62,7 +62,7 @@ installcheck: all
 	$(pg_isolation_regress_installcheck) --schedule=$(srcdir)/isolation_schedule
 
 check: all
-	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule
+	$(pg_isolation_regress_check) timeouts
 
 # Non-default tests.  It only makes sense to run these if set up to use
 # prepared transactions, via TEMP_CONFIG for the check case, or via the
@@ -72,3 +72,6 @@ installcheck-prepared-txns: all temp-install
 
 check-prepared-txns: all temp-install
 	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule prepared-transactions prepared-transactions-cic
+
+check-timeouts: all temp-install
+	$(pg_isolation_regress_check) timeouts
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..5b06148cee 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 6 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,64 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stto s3_begin sleep s3_check abort
+step stto: SET statement_timeout = '1ms'; SET transaction_timeout = '1s';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep: SELECT pg_sleep(0.01);
+ERROR:  canceling statement due to statement timeout
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    1
+(1 row)
+
+step abort: ABORT;
+
+starting permutation: tsto s3_begin wait_check s3_check
+step tsto: SET statement_timeout = '1s'; SET transaction_timeout = '1ms';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step wait_check: SELECT pg_sleep(0.01);
+pg_sleep
+--------
+        
+(1 row)
+
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: itto s4_begin wait_check s4_check
+step itto: SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s';
+step s4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step wait_check: SELECT pg_sleep(0.01);
+pg_sleep
+--------
+        
+(1 row)
+
+step s4_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: tito s5_begin wait_check s5_check
+step tito: SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms';
+step s5_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step wait_check: SELECT pg_sleep(0.01);
+pg_sleep
+--------
+        
+(1 row)
+
+step s5_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5';
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..0cca6ff147 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -27,6 +27,27 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session s3
+step s3_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step stto	{ SET statement_timeout = '1ms'; SET transaction_timeout = '1s'; }
+step tsto	{ SET statement_timeout = '1s'; SET transaction_timeout = '1ms'; }
+step sleep	{ SELECT pg_sleep(0.01); }
+step abort	{ ABORT; }
+
+session s4
+step s4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step itto	{ SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s'; }
+
+session s5
+step s5_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step tito	{ SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms'; }
+
+session s6
+step wait_check	{ SELECT pg_sleep(0.01); }
+step s3_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3'; }
+step s4_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4'; }
+step s5_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5'; }
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +68,12 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# statement timeout expires first
+permutation stto s3_begin sleep s3_check abort
+# transaction timeout expires first, session s3 FATAL-out
+permutation tsto s3_begin wait_check s3_check
+# idle in transaction timeout expires first, session s4 FATAL-out
+permutation itto s4_begin wait_check s4_check
+# transaction timeout expires first, session s5 FATAL-out
+permutation tito s5_begin wait_check s5_check
-- 
2.41.0



  [application/octet-stream] v15-0002-set-transaction_timeout-before-next-command.patch (2.3K, ../../CAEG8a3K8kD0mQDFzj=v0zV7h+m4J4TDBLkXSJJS3+VGWNvnDCQ@mail.gmail.com/3-v15-0002-set-transaction_timeout-before-next-command.patch)
  download | inline diff:
From 543c809ac2086d421fc48792a6f197ac35f06fa4 Mon Sep 17 00:00:00 2001
From: Zhao Junwang <[email protected]>
Date: Fri, 22 Dec 2023 23:13:07 +0800
Subject: [PATCH v15 2/2] set transaction_timeout before next command

Signed-off-by: Zhao Junwang <[email protected]>
---
 src/backend/tcop/postgres.c | 17 ++++++++++++-----
 1 file changed, 12 insertions(+), 5 deletions(-)

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index a2611cf8e6..75f5b21dc0 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,10 +2745,6 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
-		/* Schedule or reschedule transaction timeout */
-		if (TransactionTimeout > 0)
-			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
-
 		xact_started = true;
 	}
 
@@ -4144,6 +4140,7 @@ PostgresMain(const char *dbname, const char *username)
 	volatile bool send_ready_for_query = true;
 	volatile bool idle_in_transaction_timeout_enabled = false;
 	volatile bool idle_session_timeout_enabled = false;
+	volatile bool transaction_timeout_enabled = false;
 
 	Assert(dbname != NULL);
 	Assert(username != NULL);
@@ -4357,6 +4354,7 @@ PostgresMain(const char *dbname, const char *username)
 		QueryCancelPending = false;
 		idle_in_transaction_timeout_enabled = false;
 		idle_session_timeout_enabled = false;
+		transaction_timeout_enabled = false;
 
 		/* Not reading from the client anymore. */
 		DoingCommandRead = false;
@@ -4527,6 +4525,13 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (!transaction_timeout_enabled && TransactionTimeout > 0)
+				{
+					enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+					transaction_timeout_enabled = true;
+				}
 			}
 			else
 			{
@@ -4580,8 +4585,10 @@ PostgresMain(const char *dbname, const char *username)
 										 IdleSessionTimeout);
 				}
 
-				if (get_timeout_active(TRANSACTION_TIMEOUT))
+				if (transaction_timeout_enabled) {
 					disable_timeout(TRANSACTION_TIMEOUT, false);
+					transaction_timeout_enabled = false;
+				}
 			}
 
 			/* Report any recently-changed GUC options */
-- 
2.41.0



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-23 00:32  Japin Li <[email protected]>
  parent: Junwang Zhao <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Japin Li @ 2023-12-23 00:32 UTC (permalink / raw)
  To: Junwang Zhao <[email protected]>; +Cc: Andrey M. Borodin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected]


On Fri, 22 Dec 2023 at 23:30, Junwang Zhao <[email protected]> wrote:
> On Fri, Dec 22, 2023 at 10:44 PM Japin Li <[email protected]> wrote:
>>
>>
>> On Fri, 22 Dec 2023 at 22:37, Junwang Zhao <[email protected]> wrote:
>> > On Fri, Dec 22, 2023 at 10:25 PM Japin Li <[email protected]> wrote:
>> >> I try to set idle_in_transaction_session_timeout after begin transaction,
>> >> it changes immediately, so I think transaction_timeout should also be take
>> >> immediately.
>> >
>> > Ah, right, idle_in_transaction_session_timeout is set after the set
>> > command finishes and before the backend send *ready for query*
>> > to the client, so the value of the GUC is already set before
>> > next command.
>> >
>>
>> I mean, is it possible to set transaction_timeout before next comand?
>>
> Yeah, it's possible, set transaction_timeout in the when it first
> goes into *idle in transaction* mode, see the attached files.
>

Thanks for updating the patch, LGTM.

--
Regrads,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-23 02:40  Japin Li <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 2 replies; 76+ messages in thread

From: Japin Li @ 2023-12-23 02:40 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: Junwang Zhao <[email protected]>; Andrey M. Borodin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected]


On Sat, 23 Dec 2023 at 08:32, Japin Li <[email protected]> wrote:
> On Fri, 22 Dec 2023 at 23:30, Junwang Zhao <[email protected]> wrote:
>> On Fri, Dec 22, 2023 at 10:44 PM Japin Li <[email protected]> wrote:
>>>
>>>
>>> On Fri, 22 Dec 2023 at 22:37, Junwang Zhao <[email protected]> wrote:
>>> > On Fri, Dec 22, 2023 at 10:25 PM Japin Li <[email protected]> wrote:
>>> >> I try to set idle_in_transaction_session_timeout after begin transaction,
>>> >> it changes immediately, so I think transaction_timeout should also be take
>>> >> immediately.
>>> >
>>> > Ah, right, idle_in_transaction_session_timeout is set after the set
>>> > command finishes and before the backend send *ready for query*
>>> > to the client, so the value of the GUC is already set before
>>> > next command.
>>> >
>>>
>>> I mean, is it possible to set transaction_timeout before next comand?
>>>
>> Yeah, it's possible, set transaction_timeout in the when it first
>> goes into *idle in transaction* mode, see the attached files.
>>
>
> Thanks for updating the patch, LGTM.

Sorry for the noise!

Read the previous threads, I find why the author enable transaction_timeout
in start_xact_command().

The v15 patch cannot handle COMMIT AND CHAIN, see [1]. For example:

SET transaction_timeout TO '2s'; BEGIN; SELECT 1, pg_sleep(1); COMMIT AND CHAIN; SELECT 2, pg_sleep(1); COMMIT;

The transaction_timeout do not reset when executing COMMIT AND CHAIN.

[1] https://www.postgresql.org/message-id/a906dea1-76a1-4f26-76c5-a7efad3ef5b8%40oss.nttdata.com

--
Regrads,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-23 03:08  Junwang Zhao <[email protected]>
  parent: Japin Li <[email protected]>
  1 sibling, 0 replies; 76+ messages in thread

From: Junwang Zhao @ 2023-12-23 03:08 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: Andrey M. Borodin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected]

On Sat, Dec 23, 2023 at 10:40 AM Japin Li <[email protected]> wrote:
>
>
> On Sat, 23 Dec 2023 at 08:32, Japin Li <[email protected]> wrote:
> > On Fri, 22 Dec 2023 at 23:30, Junwang Zhao <[email protected]> wrote:
> >> On Fri, Dec 22, 2023 at 10:44 PM Japin Li <[email protected]> wrote:
> >>>
> >>>
> >>> On Fri, 22 Dec 2023 at 22:37, Junwang Zhao <[email protected]> wrote:
> >>> > On Fri, Dec 22, 2023 at 10:25 PM Japin Li <[email protected]> wrote:
> >>> >> I try to set idle_in_transaction_session_timeout after begin transaction,
> >>> >> it changes immediately, so I think transaction_timeout should also be take
> >>> >> immediately.
> >>> >
> >>> > Ah, right, idle_in_transaction_session_timeout is set after the set
> >>> > command finishes and before the backend send *ready for query*
> >>> > to the client, so the value of the GUC is already set before
> >>> > next command.
> >>> >
> >>>
> >>> I mean, is it possible to set transaction_timeout before next comand?
> >>>
> >> Yeah, it's possible, set transaction_timeout in the when it first
> >> goes into *idle in transaction* mode, see the attached files.
> >>
> >
> > Thanks for updating the patch, LGTM.
>
> Sorry for the noise!
>
> Read the previous threads, I find why the author enable transaction_timeout
> in start_xact_command().
>
> The v15 patch cannot handle COMMIT AND CHAIN, see [1]. For example:

I didn't read the previous threads, sorry for that, let's stick to v14.

>
> SET transaction_timeout TO '2s'; BEGIN; SELECT 1, pg_sleep(1); COMMIT AND CHAIN; SELECT 2, pg_sleep(1); COMMIT;
>
> The transaction_timeout do not reset when executing COMMIT AND CHAIN.
>
> [1] https://www.postgresql.org/message-id/a906dea1-76a1-4f26-76c5-a7efad3ef5b8%40oss.nttdata.com
>
> --
> Regrads,
> Japin Li
> ChengDu WenWu Information Technology Co., Ltd.



-- 
Regards
Junwang Zhao






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-23 03:17  Japin Li <[email protected]>
  parent: Japin Li <[email protected]>
  1 sibling, 1 reply; 76+ messages in thread

From: Japin Li @ 2023-12-23 03:17 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: Junwang Zhao <[email protected]>; Andrey M. Borodin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected]

a
On Sat, 23 Dec 2023 at 10:40, Japin Li <[email protected]> wrote:
> On Sat, 23 Dec 2023 at 08:32, Japin Li <[email protected]> wrote:
>> On Fri, 22 Dec 2023 at 23:30, Junwang Zhao <[email protected]> wrote:
>>> On Fri, Dec 22, 2023 at 10:44 PM Japin Li <[email protected]> wrote:
>>>>
>>>>
>>>> On Fri, 22 Dec 2023 at 22:37, Junwang Zhao <[email protected]> wrote:
>>>> > On Fri, Dec 22, 2023 at 10:25 PM Japin Li <[email protected]> wrote:
>>>> >> I try to set idle_in_transaction_session_timeout after begin transaction,
>>>> >> it changes immediately, so I think transaction_timeout should also be take
>>>> >> immediately.
>>>> >
>>>> > Ah, right, idle_in_transaction_session_timeout is set after the set
>>>> > command finishes and before the backend send *ready for query*
>>>> > to the client, so the value of the GUC is already set before
>>>> > next command.
>>>> >
>>>>
>>>> I mean, is it possible to set transaction_timeout before next comand?
>>>>
>>> Yeah, it's possible, set transaction_timeout in the when it first
>>> goes into *idle in transaction* mode, see the attached files.
>>>
>>
>> Thanks for updating the patch, LGTM.
>
> Sorry for the noise!
>
> Read the previous threads, I find why the author enable transaction_timeout
> in start_xact_command().
>
> The v15 patch cannot handle COMMIT AND CHAIN, see [1]. For example:
>
> SET transaction_timeout TO '2s'; BEGIN; SELECT 1, pg_sleep(1); COMMIT AND CHAIN; SELECT 2, pg_sleep(1); COMMIT;
>
> The transaction_timeout do not reset when executing COMMIT AND CHAIN.
>
> [1] https://www.postgresql.org/message-id/a906dea1-76a1-4f26-76c5-a7efad3ef5b8%40oss.nttdata.com

Attach v16 to solve this. Any suggestions?

--
Regrads,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.



Attachments:

  [text/x-diff] v16-0001-Introduce-transaction_timeout.patch (20.6K, ../../ME3P282MB316691F6AD09F4F8EA5578C1B69BA@ME3P282MB3166.AUSP282.PROD.OUTLOOK.COM/2-v16-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From a9d79c5a013da8fa707556f87a34ff3ade779729 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v16 1/2] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>
Reviewed-by: Japin Li <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 35 +++++++++++
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 27 +++++++-
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 +++
 src/backend/utils/misc/guc_tables.c           | 11 ++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/Makefile                   |  5 +-
 src/test/isolation/expected/timeouts.out      | 63 ++++++++++++++++++-
 src/test/isolation/specs/timeouts.spec        | 30 +++++++++
 18 files changed, 190 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index b5624ca884..d62edcf83b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9134,6 +9134,41 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index b04fcfc8c8..e6fa1cfdc2 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b6451d9d08..4be06c1e5d 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7298a187d1..a2611cf8e6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,6 +2745,10 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		/* Schedule or reschedule transaction timeout */
+		if (TransactionTimeout > 0)
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -3426,6 +3430,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,7 +4506,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4504,7 +4520,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4562,6 +4579,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5140,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 8e97a0150f..8f1157afee 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 60bc1217fb..fd586c193c 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 552cf9d950..64be4de0c7 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 9f59440526..c5cfbef02b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2556,6 +2556,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index b2809c711a..0b37117eb7 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -700,6 +700,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4..3342971bd0 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8c0b5486b9..21bd16ef00 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1251,6 +1251,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 417c74cfef..9cda3f3667 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 74bc2f97cb..b2d0f84252 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index e87fd25d64..9dde9cbfdd 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -429,6 +429,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 8a61853371..608a83d5a8 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/Makefile b/src/test/isolation/Makefile
index ade2256ed3..2bd06f8f15 100644
--- a/src/test/isolation/Makefile
+++ b/src/test/isolation/Makefile
@@ -62,7 +62,7 @@ installcheck: all
 	$(pg_isolation_regress_installcheck) --schedule=$(srcdir)/isolation_schedule
 
 check: all
-	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule
+	$(pg_isolation_regress_check) timeouts
 
 # Non-default tests.  It only makes sense to run these if set up to use
 # prepared transactions, via TEMP_CONFIG for the check case, or via the
@@ -72,3 +72,6 @@ installcheck-prepared-txns: all temp-install
 
 check-prepared-txns: all temp-install
 	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule prepared-transactions prepared-transactions-cic
+
+check-timeouts: all temp-install
+	$(pg_isolation_regress_check) timeouts
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..5b06148cee 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 6 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,64 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stto s3_begin sleep s3_check abort
+step stto: SET statement_timeout = '1ms'; SET transaction_timeout = '1s';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep: SELECT pg_sleep(0.01);
+ERROR:  canceling statement due to statement timeout
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    1
+(1 row)
+
+step abort: ABORT;
+
+starting permutation: tsto s3_begin wait_check s3_check
+step tsto: SET statement_timeout = '1s'; SET transaction_timeout = '1ms';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step wait_check: SELECT pg_sleep(0.01);
+pg_sleep
+--------
+        
+(1 row)
+
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: itto s4_begin wait_check s4_check
+step itto: SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s';
+step s4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step wait_check: SELECT pg_sleep(0.01);
+pg_sleep
+--------
+        
+(1 row)
+
+step s4_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: tito s5_begin wait_check s5_check
+step tito: SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms';
+step s5_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step wait_check: SELECT pg_sleep(0.01);
+pg_sleep
+--------
+        
+(1 row)
+
+step s5_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5';
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..0cca6ff147 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -27,6 +27,27 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session s3
+step s3_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step stto	{ SET statement_timeout = '1ms'; SET transaction_timeout = '1s'; }
+step tsto	{ SET statement_timeout = '1s'; SET transaction_timeout = '1ms'; }
+step sleep	{ SELECT pg_sleep(0.01); }
+step abort	{ ABORT; }
+
+session s4
+step s4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step itto	{ SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s'; }
+
+session s5
+step s5_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step tito	{ SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms'; }
+
+session s6
+step wait_check	{ SELECT pg_sleep(0.01); }
+step s3_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3'; }
+step s4_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4'; }
+step s5_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5'; }
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +68,12 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# statement timeout expires first
+permutation stto s3_begin sleep s3_check abort
+# transaction timeout expires first, session s3 FATAL-out
+permutation tsto s3_begin wait_check s3_check
+# idle in transaction timeout expires first, session s4 FATAL-out
+permutation itto s4_begin wait_check s4_check
+# transaction timeout expires first, session s5 FATAL-out
+permutation tito s5_begin wait_check s5_check

base-commit: 3e2e0d5ad7fcb89d18a71cbfc885ef184e1b6f2e
-- 
2.41.0



  [text/x-diff] v16-0002-Try-to-enable-transaction_timeout-before-next-co.patch (1.4K, ../../ME3P282MB316691F6AD09F4F8EA5578C1B69BA@ME3P282MB3166.AUSP282.PROD.OUTLOOK.COM/3-v16-0002-Try-to-enable-transaction_timeout-before-next-co.patch)
  download | inline diff:
From 95133524aaeaf84733d6bb12a42d3e27aea85c16 Mon Sep 17 00:00:00 2001
From: japinli <[email protected]>
Date: Sat, 23 Dec 2023 11:04:25 +0800
Subject: [PATCH v16 2/2] Try to enable transaction_timeout before next command

---
 src/backend/tcop/postgres.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index a2611cf8e6..96161eb7ab 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -4513,6 +4513,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else if (IsTransactionOrTransactionBlock())
 			{
@@ -4527,6 +4532,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else
 			{
-- 
2.41.0



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-23 03:35  Junwang Zhao <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Junwang Zhao @ 2023-12-23 03:35 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: Andrey M. Borodin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected]

On Sat, Dec 23, 2023 at 11:17 AM Japin Li <[email protected]> wrote:
>
> a
> On Sat, 23 Dec 2023 at 10:40, Japin Li <[email protected]> wrote:
> > On Sat, 23 Dec 2023 at 08:32, Japin Li <[email protected]> wrote:
> >> On Fri, 22 Dec 2023 at 23:30, Junwang Zhao <[email protected]> wrote:
> >>> On Fri, Dec 22, 2023 at 10:44 PM Japin Li <[email protected]> wrote:
> >>>>
> >>>>
> >>>> On Fri, 22 Dec 2023 at 22:37, Junwang Zhao <[email protected]> wrote:
> >>>> > On Fri, Dec 22, 2023 at 10:25 PM Japin Li <[email protected]> wrote:
> >>>> >> I try to set idle_in_transaction_session_timeout after begin transaction,
> >>>> >> it changes immediately, so I think transaction_timeout should also be take
> >>>> >> immediately.
> >>>> >
> >>>> > Ah, right, idle_in_transaction_session_timeout is set after the set
> >>>> > command finishes and before the backend send *ready for query*
> >>>> > to the client, so the value of the GUC is already set before
> >>>> > next command.
> >>>> >
> >>>>
> >>>> I mean, is it possible to set transaction_timeout before next comand?
> >>>>
> >>> Yeah, it's possible, set transaction_timeout in the when it first
> >>> goes into *idle in transaction* mode, see the attached files.
> >>>
> >>
> >> Thanks for updating the patch, LGTM.
> >
> > Sorry for the noise!
> >
> > Read the previous threads, I find why the author enable transaction_timeout
> > in start_xact_command().
> >
> > The v15 patch cannot handle COMMIT AND CHAIN, see [1]. For example:
> >
> > SET transaction_timeout TO '2s'; BEGIN; SELECT 1, pg_sleep(1); COMMIT AND CHAIN; SELECT 2, pg_sleep(1); COMMIT;
> >
> > The transaction_timeout do not reset when executing COMMIT AND CHAIN.
> >
> > [1] https://www.postgresql.org/message-id/a906dea1-76a1-4f26-76c5-a7efad3ef5b8%40oss.nttdata.com
>
> Attach v16 to solve this. Any suggestions?

I've checked this with *COMMIT AND CHAIN* and *ABORT AND CHAIN*,
both work as expected. Thanks for the update.

>
> --
> Regrads,
> Japin Li
> ChengDu WenWu Information Technology Co., Ltd.
>


-- 
Regards
Junwang Zhao






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-23 06:48  Li Japin <[email protected]>
  parent: Junwang Zhao <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Li Japin @ 2023-12-23 06:48 UTC (permalink / raw)
  To: Junwang Zhao <[email protected]>; +Cc: Andrey M. Borodin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>



> 在 2023年12月23日,11:35,Junwang Zhao <[email protected]> 写道:
> 
> On Sat, Dec 23, 2023 at 11:17 AM Japin Li <[email protected]> wrote:
>> 
>> a
>>> On Sat, 23 Dec 2023 at 10:40, Japin Li <[email protected]> wrote:
>>> On Sat, 23 Dec 2023 at 08:32, Japin Li <[email protected]> wrote:
>>>> On Fri, 22 Dec 2023 at 23:30, Junwang Zhao <[email protected]> wrote:
>>>>> On Fri, Dec 22, 2023 at 10:44 PM Japin Li <[email protected]> wrote:
>>>>>> 
>>>>>> 
>>>>>> On Fri, 22 Dec 2023 at 22:37, Junwang Zhao <[email protected]> wrote:
>>>>>>> On Fri, Dec 22, 2023 at 10:25 PM Japin Li <[email protected]> wrote:
>>>>>>>> I try to set idle_in_transaction_session_timeout after begin transaction,
>>>>>>>> it changes immediately, so I think transaction_timeout should also be take
>>>>>>>> immediately.
>>>>>>> 
>>>>>>> Ah, right, idle_in_transaction_session_timeout is set after the set
>>>>>>> command finishes and before the backend send *ready for query*
>>>>>>> to the client, so the value of the GUC is already set before
>>>>>>> next command.
>>>>>>> 
>>>>>> 
>>>>>> I mean, is it possible to set transaction_timeout before next comand?
>>>>>> 
>>>>> Yeah, it's possible, set transaction_timeout in the when it first
>>>>> goes into *idle in transaction* mode, see the attached files.
>>>>> 
>>>> 
>>>> Thanks for updating the patch, LGTM.
>>> 
>>> Sorry for the noise!
>>> 
>>> Read the previous threads, I find why the author enable transaction_timeout
>>> in start_xact_command().
>>> 
>>> The v15 patch cannot handle COMMIT AND CHAIN, see [1]. For example:
>>> 
>>> SET transaction_timeout TO '2s'; BEGIN; SELECT 1, pg_sleep(1); COMMIT AND CHAIN; SELECT 2, pg_sleep(1); COMMIT;
>>> 
>>> The transaction_timeout do not reset when executing COMMIT AND CHAIN.
>>> 
>>> [1] https://www.postgresql.org/message-id/a906dea1-76a1-4f26-76c5-a7efad3ef5b8%40oss.nttdata.com
>> 
>> Attach v16 to solve this. Any suggestions?
> 
> I've checked this with *COMMIT AND CHAIN* and *ABORT AND CHAIN*,
> both work as expected. Thanks for the update.
> 

Thanks for your testing and reviewing!

^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-23 17:14  Andrey M. Borodin <[email protected]>
  parent: Li Japin <[email protected]>
  0 siblings, 3 replies; 76+ messages in thread

From: Andrey M. Borodin @ 2023-12-23 17:14 UTC (permalink / raw)
  To: Li Japin <[email protected]>; +Cc: Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>



> On 22 Dec 2023, at 10:39, Japin Li <[email protected]> wrote:
> 
> 
> I try to split the test for transaction timeout, and all passed on my CI [1].


I like the refactoring you did in timeout.spec. I thought it is impossible, because permutations would try to reinitialize FATALed sessions. But, obviously, tests work the way you refactored it.
However I don't think ignoring test failures on Windows without understanding root cause is a good idea.
Let's get back to v13 version of tests, understand why it failed, apply your test refactorings afterwards. BTW are you sure that v14 refactorings are functional equivalent of v13 tests?

To go with this plan I attach slightly modified version of v13 tests in v16 patchset. The only change is timing in "sleep_there" step. I suspect that failure was induced by more coarse timer granularity on Windows. Tests were giving only 9 milliseconds for a timeout to entirely wipe away backend from pg_stat_activity. This saves testing time, but might induce false positive test flaps. So I've raised wait times to 100ms. This seems too much, but I do not have other ideas how to ensure tests stability. Maybe 50ms would be enough, I do not know. Isolation runs ~50 seconds now. I'm tempted to say that 200ms for timeouts worth it.


As to 2nd step "Try to enable transaction_timeout during transaction", I think this makes sense. But if we are doing so, shouldn't we also allow to enable idle_in_transaction timeout in a same manner? Currently we only allow to disable other timeouts... Also, if we are already in transaction, shouldn't we also subtract current transaction span from timeout?
I think making this functionality as another step of the patchset was a good idea.

Thanks!


Best regards, Andrey Borodin.


Attachments:

  [application/octet-stream] v17-0001-Introduce-transaction_timeout.patch (19.8K, ../../[email protected]/2-v17-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From a4f37d1ba58429799e45c51a88e0821674740ac7 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v17 1/2] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>
Reviewed-by: Japin Li <[email protected]>
Reviewed-by: Junwang Zhao <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 35 ++++++++++++++++
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 27 ++++++++++--
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 +++++
 src/backend/utils/misc/guc_tables.c           | 11 +++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/expected/timeouts.out      | 41 ++++++++++++++++++-
 src/test/isolation/specs/timeouts.spec        | 29 ++++++++++++-
 17 files changed, 162 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index b5624ca884..d62edcf83b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9134,6 +9134,41 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index b04fcfc8c8..e6fa1cfdc2 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b6451d9d08..4be06c1e5d 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7298a187d1..a2611cf8e6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,6 +2745,10 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		/* Schedule or reschedule transaction timeout */
+		if (TransactionTimeout > 0)
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -3426,6 +3430,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,7 +4506,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4504,7 +4520,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4562,6 +4579,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5140,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 8e97a0150f..8f1157afee 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 60bc1217fb..fd586c193c 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 552cf9d950..64be4de0c7 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 9f59440526..c5cfbef02b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2556,6 +2556,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index b2809c711a..0b37117eb7 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -700,6 +700,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4..3342971bd0 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8c0b5486b9..21bd16ef00 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1251,6 +1251,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 417c74cfef..9cda3f3667 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 74bc2f97cb..b2d0f84252 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index e87fd25d64..9dde9cbfdd 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -429,6 +429,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 8a61853371..608a83d5a8 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..cabe28f2c8 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 6 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,42 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4
+step stt1_set: SET transaction_timeout = '1ms';
+step stt1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_here: SELECT pg_sleep(1);
+FATAL:  terminating connection due to transaction timeout
+server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+
+step stt2_set: SET transaction_timeout = '1ms';
+step stt2_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
+count
+-----
+    0
+(1 row)
+
+step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
+step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
+step stt3_check_itt4: <... completed>
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..2772939b6b 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -18,7 +18,7 @@ step wrtbl	{ UPDATE accounts SET balance = balance + 100; }
 teardown	{ ABORT; }
 
 session s2
-setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+setup		{ SET transaction_timeout = '10s'; SET idle_in_transaction_session_timeout = '10s'; BEGIN ISOLATION LEVEL READ COMMITTED; }
 step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
@@ -27,6 +27,29 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session stt1
+# enable statement_timeout to check interaction
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt1_set	{ SET transaction_timeout = '1ms'; }
+step stt1_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step sleep_here	{ SELECT pg_sleep(1); }
+
+session stt2
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt2_set	{ SET transaction_timeout = '1ms'; }
+step stt2_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+# Session stt2 is terminated in the background. However, isolation tester needs a step to observe it.
+
+session stt3
+step sleep_there{ SELECT pg_sleep(0.1); }
+# Observe that stt2\itt4 died
+step stt3_check_stt2 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2' }
+step stt3_check_itt4 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' }
+
+session itt4
+step itt4_set	{ SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s'; }
+step itt4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +70,7 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# timeout of active query, idle transaction timeout
+permutation stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4(*)
+# can't run tests after this, sessions stt1, stt2, and itt4 are expected to FATAL-out
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v17-0002-Try-to-enable-transaction_timeout-before-next-co.patch (1.4K, ../../[email protected]/3-v17-0002-Try-to-enable-transaction_timeout-before-next-co.patch)
  download | inline diff:
From 0633006abc33608796a9f90eb851a129d996045d Mon Sep 17 00:00:00 2001
From: japinli <[email protected]>
Date: Sat, 23 Dec 2023 11:04:25 +0800
Subject: [PATCH v17 2/2] Try to enable transaction_timeout before next command

---
 src/backend/tcop/postgres.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index a2611cf8e6..96161eb7ab 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -4513,6 +4513,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else if (IsTransactionOrTransactionBlock())
 			{
@@ -4527,6 +4532,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else
 			{
-- 
2.37.1 (Apple Git-137.1)



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-25 02:17  Junwang Zhao <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  2 siblings, 0 replies; 76+ messages in thread

From: Junwang Zhao @ 2023-12-25 02:17 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: Li Japin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>

Hi Andrey,

On Sun, Dec 24, 2023 at 1:14 AM Andrey M. Borodin <[email protected]> wrote:
>
>
>
> > On 22 Dec 2023, at 10:39, Japin Li <[email protected]> wrote:
> >
> >
> > I try to split the test for transaction timeout, and all passed on my CI [1].
>
>
> I like the refactoring you did in timeout.spec. I thought it is impossible, because permutations would try to reinitialize FATALed sessions. But, obviously, tests work the way you refactored it.
> However I don't think ignoring test failures on Windows without understanding root cause is a good idea.
> Let's get back to v13 version of tests, understand why it failed, apply your test refactorings afterwards. BTW are you sure that v14 refactorings are functional equivalent of v13 tests?
>
> To go with this plan I attach slightly modified version of v13 tests in v16 patchset. The only change is timing in "sleep_there" step. I suspect that failure was induced by more coarse timer granularity on Windows. Tests were giving only 9 milliseconds for a timeout to entirely wipe away backend from pg_stat_activity. This saves testing time, but might induce false positive test flaps. So I've raised wait times to 100ms. This seems too much, but I do not have other ideas how to ensure tests stability. Maybe 50ms would be enough, I do not know. Isolation runs ~50 seconds now. I'm tempted to say that 200ms for timeouts worth it.
>
>
> As to 2nd step "Try to enable transaction_timeout during transaction", I think this makes sense. But if we are doing so, shouldn't we also allow to enable idle_in_transaction timeout in a same manner? Currently we only allow to disable other timeouts... Also, if we are already in transaction, shouldn't we also subtract current transaction span from timeout?
idle_in_transaction_session_timeout is already the behavior Japin suggested,
it is enabled before backend sends *read for query* to client.

> I think making this functionality as another step of the patchset was a good idea.
>
> Thanks!
>
>
> Best regards, Andrey Borodin.



-- 
Regards
Junwang Zhao






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-25 02:27  Japin Li <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  2 siblings, 0 replies; 76+ messages in thread

From: Japin Li @ 2023-12-25 02:27 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>


On Sun, 24 Dec 2023 at 01:14, Andrey M. Borodin <[email protected]> wrote:
>> On 22 Dec 2023, at 10:39, Japin Li <[email protected]> wrote:
>>
>>
>> I try to split the test for transaction timeout, and all passed on my CI [1].
>
>
> I like the refactoring you did in timeout.spec. I thought it is impossible, because permutations would try to reinitialize FATALed sessions. But, obviously, tests work the way you refactored it.
> However I don't think ignoring test failures on Windows without understanding root cause is a good idea.

Yeah.

> Let's get back to v13 version of tests, understand why it failed, apply your test refactorings afterwards. BTW are you sure that v14 refactorings are functional equivalent of v13 tests?
>
I think it is equivalent.  Maybe I missing something.  Please let me known
if they are not equivalent.

> To go with this plan I attach slightly modified version of v13 tests in v16 patchset. The only change is timing in "sleep_there" step. I suspect that failure was induced by more coarse timer granularity on Windows. Tests were giving only 9 milliseconds for a timeout to entirely wipe away backend from pg_stat_activity. This saves testing time, but might induce false positive test flaps. So I've raised wait times to 100ms. This seems too much, but I do not have other ideas how to ensure tests stability. Maybe 50ms would be enough, I do not know. Isolation runs ~50 seconds now. I'm tempted to say that 200ms for timeouts worth it.
>
So this is caused by Windows timer granularity?

> As to 2nd step "Try to enable transaction_timeout during transaction", I think this makes sense. But if we are doing so, shouldn't we also allow to enable idle_in_transaction timeout in a same manner?

I think the current idle_in_transaction_session_timeout work correctly.

> Currently we only allow to disable other timeouts... Also, if we are already in transaction, shouldn't we also subtract current transaction span from timeout?

Agreed.

> I think making this functionality as another step of the patchset was a good idea.
>

--
Regrads,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-28 16:02  Junwang Zhao <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  2 siblings, 1 reply; 76+ messages in thread

From: Junwang Zhao @ 2023-12-28 16:02 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: Li Japin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>

Hey Andrey,

On Sun, Dec 24, 2023 at 1:14 AM Andrey M. Borodin <[email protected]> wrote:
>
>
>
> > On 22 Dec 2023, at 10:39, Japin Li <[email protected]> wrote:
> >
> >
> > I try to split the test for transaction timeout, and all passed on my CI [1].
>
>
> I like the refactoring you did in timeout.spec. I thought it is impossible, because permutations would try to reinitialize FATALed sessions. But, obviously, tests work the way you refactored it.
> However I don't think ignoring test failures on Windows without understanding root cause is a good idea.
> Let's get back to v13 version of tests, understand why it failed, apply your test refactorings afterwards. BTW are you sure that v14 refactorings are functional equivalent of v13 tests?
>
> To go with this plan I attach slightly modified version of v13 tests in v16 patchset. The only change is timing in "sleep_there" step. I suspect that failure was induced by more coarse timer granularity on Windows. Tests were giving only 9 milliseconds for a timeout to entirely wipe away backend from pg_stat_activity. This saves testing time, but might induce false positive test flaps. So I've raised wait times to 100ms. This seems too much, but I do not have other ideas how to ensure tests stability. Maybe 50ms would be enough, I do not know. Isolation runs ~50 seconds now. I'm tempted to say that 200ms for timeouts worth it.
>
>
> As to 2nd step "Try to enable transaction_timeout during transaction", I think this makes sense. But if we are doing so, shouldn't we also allow to enable idle_in_transaction timeout in a same manner? Currently we only allow to disable other timeouts... Also, if we are already in transaction, shouldn't we also subtract current transaction span from timeout?
> I think making this functionality as another step of the patchset was a good idea.
>
> Thanks!
Seems V5~V17 doesn't work as expected for Nikolay's case:

postgres=# set transaction_timeout to '2s';
SET
postgres=# begin; select pg_sleep(1); select pg_sleep(1); select
pg_sleep(1); select pg_sleep(1); select pg_sleep(1); commit;
BEGIN

The reason for this seems the timer has been refreshed for each
command, xact_started along can not indicate it's a new
transaction or not, there is a TransactionState contains some
infos.

So I propose the following change, what do you think?

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index a2611cf8e6..cffd2c44d0 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2746,7 +2746,7 @@ start_xact_command(void)
                StartTransactionCommand();

                /* Schedule or reschedule transaction timeout */
-               if (TransactionTimeout > 0)
+               if (TransactionTimeout > 0 &&
!get_timeout_active(TRANSACTION_TIMEOUT))
                        enable_timeout_after(TRANSACTION_TIMEOUT,
TransactionTimeout);

                xact_started = true;

>
>
> Best regards, Andrey Borodin.



-- 
Regards
Junwang Zhao






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-29 10:00  Andrey M. Borodin <[email protected]>
  parent: Junwang Zhao <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2023-12-29 10:00 UTC (permalink / raw)
  To: Junwang Zhao <[email protected]>; +Cc: Li Japin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>



> On 28 Dec 2023, at 21:02, Junwang Zhao <[email protected]> wrote:
> 
> Seems V5~V17 doesn't work as expected for Nikolay's case:
> 

Yeah, that's a problem.
> So I propose the following change, what do you think?
This breaks COMMIT AND CHAIN.

PFA v18: I've added a test for Nik's case and for COMMIT AND CHAIN. Now we need to fix stuff to pass this tests (I've crafted output).
We also need test for patchset step "Try to enable transaction_timeout before next command".

Thanks!


Best regards, Andrey Borodin.


Attachments:

  [application/octet-stream] v18-0001-Introduce-transaction_timeout.patch (19.8K, ../../[email protected]/2-v18-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From a4f37d1ba58429799e45c51a88e0821674740ac7 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v18 1/3] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>
Reviewed-by: Japin Li <[email protected]>
Reviewed-by: Junwang Zhao <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 35 ++++++++++++++++
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 27 ++++++++++--
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 +++++
 src/backend/utils/misc/guc_tables.c           | 11 +++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/expected/timeouts.out      | 41 ++++++++++++++++++-
 src/test/isolation/specs/timeouts.spec        | 29 ++++++++++++-
 17 files changed, 162 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index b5624ca884..d62edcf83b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9134,6 +9134,41 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index b04fcfc8c8..e6fa1cfdc2 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b6451d9d08..4be06c1e5d 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7298a187d1..a2611cf8e6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,6 +2745,10 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		/* Schedule or reschedule transaction timeout */
+		if (TransactionTimeout > 0)
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -3426,6 +3430,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,7 +4506,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4504,7 +4520,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4562,6 +4579,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5140,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 8e97a0150f..8f1157afee 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 60bc1217fb..fd586c193c 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 552cf9d950..64be4de0c7 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 9f59440526..c5cfbef02b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2556,6 +2556,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index b2809c711a..0b37117eb7 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -700,6 +700,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4..3342971bd0 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8c0b5486b9..21bd16ef00 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1251,6 +1251,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 417c74cfef..9cda3f3667 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 74bc2f97cb..b2d0f84252 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index e87fd25d64..9dde9cbfdd 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -429,6 +429,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 8a61853371..608a83d5a8 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..cabe28f2c8 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 6 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,42 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4
+step stt1_set: SET transaction_timeout = '1ms';
+step stt1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_here: SELECT pg_sleep(1);
+FATAL:  terminating connection due to transaction timeout
+server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+
+step stt2_set: SET transaction_timeout = '1ms';
+step stt2_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
+count
+-----
+    0
+(1 row)
+
+step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
+step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
+step stt3_check_itt4: <... completed>
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..2772939b6b 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -18,7 +18,7 @@ step wrtbl	{ UPDATE accounts SET balance = balance + 100; }
 teardown	{ ABORT; }
 
 session s2
-setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+setup		{ SET transaction_timeout = '10s'; SET idle_in_transaction_session_timeout = '10s'; BEGIN ISOLATION LEVEL READ COMMITTED; }
 step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
@@ -27,6 +27,29 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session stt1
+# enable statement_timeout to check interaction
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt1_set	{ SET transaction_timeout = '1ms'; }
+step stt1_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step sleep_here	{ SELECT pg_sleep(1); }
+
+session stt2
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt2_set	{ SET transaction_timeout = '1ms'; }
+step stt2_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+# Session stt2 is terminated in the background. However, isolation tester needs a step to observe it.
+
+session stt3
+step sleep_there{ SELECT pg_sleep(0.1); }
+# Observe that stt2\itt4 died
+step stt3_check_stt2 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2' }
+step stt3_check_itt4 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' }
+
+session itt4
+step itt4_set	{ SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s'; }
+step itt4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +70,7 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# timeout of active query, idle transaction timeout
+permutation stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4(*)
+# can't run tests after this, sessions stt1, stt2, and itt4 are expected to FATAL-out
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v18-0002-Use-test-from-Li-Japin.patch (9.0K, ../../[email protected]/3-v18-0002-Use-test-from-Li-Japin.patch)
  download | inline diff:
From 35df782b96664f3e2422228d67b65cc243cd4723 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Fri, 29 Dec 2023 14:54:02 +0500
Subject: [PATCH v18 2/3] Use test from Li Japin Also add tests for multiple
 queries in transaction and COMMIT AND CHAIN.

---
 src/test/isolation/expected/timeouts.out | 97 +++++++++++++++++++-----
 src/test/isolation/specs/timeouts.spec   | 64 ++++++++++------
 2 files changed, 119 insertions(+), 42 deletions(-)

diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index cabe28f2c8..9fb371baf1 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 6 sessions
+Parsed test spec with 7 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -80,39 +80,98 @@ step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
 
-starting permutation: stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4
-step stt1_set: SET transaction_timeout = '1ms';
-step stt1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_here: SELECT pg_sleep(1);
-FATAL:  terminating connection due to transaction timeout
-server closed the connection unexpectedly
-	This probably means the server terminated abnormally
-	before or while processing the request.
-
-step stt2_set: SET transaction_timeout = '1ms';
-step stt2_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_there: SELECT pg_sleep(0.1);
+starting permutation: stto s3_begin sleep s3_check abort
+step stto: SET statement_timeout = '1ms'; SET transaction_timeout = '1s';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep: SELECT pg_sleep(0.1);
+ERROR:  canceling statement due to statement timeout
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    1
+(1 row)
+
+step abort: ABORT;
+
+starting permutation: tsto s3_begin wait_check s3_check
+step tsto: SET statement_timeout = '1s'; SET transaction_timeout = '1ms';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step wait_check: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: itto s4_begin wait_check s4_check
+step itto: SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s';
+step s4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step wait_check: SELECT pg_sleep(0.1);
 pg_sleep
 --------
         
 (1 row)
 
-step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
+step s4_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4';
 count
 -----
     0
 (1 row)
 
-step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
-step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_there: SELECT pg_sleep(0.1);
+
+starting permutation: tito s5_begin wait_check s5_check
+step tito: SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms';
+step s5_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step wait_check: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s5_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_select_1 wait_check s7_check
+step s7_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '150ms';
+
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_commit_and_chain: COMMIT AND CHAIN;
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_select_1: SELECT 1;
+?column?
+--------
+       1
+(1 row)
+
+step wait_check: SELECT pg_sleep(0.1);
 pg_sleep
 --------
         
 (1 row)
 
-step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
-step stt3_check_itt4: <... completed>
+step s7_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7';
 count
 -----
     0
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index 2772939b6b..5c353d4c61 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -18,7 +18,7 @@ step wrtbl	{ UPDATE accounts SET balance = balance + 100; }
 teardown	{ ABORT; }
 
 session s2
-setup		{ SET transaction_timeout = '10s'; SET idle_in_transaction_session_timeout = '10s'; BEGIN ISOLATION LEVEL READ COMMITTED; }
+setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
 step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
@@ -27,28 +27,38 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
-session stt1
-# enable statement_timeout to check interaction
-setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
-step stt1_set	{ SET transaction_timeout = '1ms'; }
-step stt1_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
-step sleep_here	{ SELECT pg_sleep(1); }
+session s3
+step s3_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step stto	{ SET statement_timeout = '1ms'; SET transaction_timeout = '1s'; }
+step tsto	{ SET statement_timeout = '1s'; SET transaction_timeout = '1ms'; }
+step sleep	{ SELECT pg_sleep(0.1); }
+step abort	{ ABORT; }
+
+session s4
+step s4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step itto	{ SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s'; }
 
-session stt2
-setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
-step stt2_set	{ SET transaction_timeout = '1ms'; }
-step stt2_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
-# Session stt2 is terminated in the background. However, isolation tester needs a step to observe it.
+session s5
+step s5_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step tito	{ SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms'; }
 
-session stt3
-step sleep_there{ SELECT pg_sleep(0.1); }
-# Observe that stt2\itt4 died
-step stt3_check_stt2 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2' }
-step stt3_check_itt4 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' }
+session s6
+step wait_check	{ SELECT pg_sleep(0.1); }
+step s3_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3'; }
+step s4_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4'; }
+step s5_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5'; }
+step s7_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7'; }
 
-session itt4
-step itt4_set	{ SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s'; }
-step itt4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+session s7
+step s7_begin
+{
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '150ms';
+}
+step s7_commit_and_chain { COMMIT AND CHAIN; }
+# to test that quick query does not restart transaction_timeout
+step s7_select_1 { SELECT 1; }
+step s7_sleep	{ SELECT pg_sleep(0.1); }
 
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
@@ -71,6 +81,14 @@ permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
 
-# timeout of active query, idle transaction timeout
-permutation stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4(*)
-# can't run tests after this, sessions stt1, stt2, and itt4 are expected to FATAL-out
+# statement timeout expires first
+permutation stto s3_begin sleep s3_check abort
+# transaction timeout expires first, session s3 FATAL-out
+permutation tsto s3_begin wait_check s3_check
+# idle in transaction timeout expires first, session s4 FATAL-out
+permutation itto s4_begin wait_check s4_check
+# transaction timeout expires first, session s5 FATAL-out
+permutation tito s5_begin wait_check s5_check
+# transaction timeout expires in presence of query flow
+# session s7 FATAL-out sleeping in last wait_check only
+permutation s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_select_1 wait_check s7_check
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v18-0003-Try-to-enable-transaction_timeout-before-next-co.patch (1.4K, ../../[email protected]/4-v18-0003-Try-to-enable-transaction_timeout-before-next-co.patch)
  download | inline diff:
From f1d62f40825b8d8989d85dee3a4152938ad94508 Mon Sep 17 00:00:00 2001
From: japinli <[email protected]>
Date: Sat, 23 Dec 2023 11:04:25 +0800
Subject: [PATCH v18 3/3] Try to enable transaction_timeout before next command

---
 src/backend/tcop/postgres.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index a2611cf8e6..96161eb7ab 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -4513,6 +4513,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else if (IsTransactionOrTransactionBlock())
 			{
@@ -4527,6 +4532,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else
 			{
-- 
2.37.1 (Apple Git-137.1)



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-29 11:00  Junwang Zhao <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Junwang Zhao @ 2023-12-29 11:00 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: Li Japin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>

On Fri, Dec 29, 2023 at 6:00 PM Andrey M. Borodin <[email protected]> wrote:
>
>
>
> > On 28 Dec 2023, at 21:02, Junwang Zhao <[email protected]> wrote:
> >
> > Seems V5~V17 doesn't work as expected for Nikolay's case:
> >
>
> Yeah, that's a problem.
> > So I propose the following change, what do you think?
> This breaks COMMIT AND CHAIN.
>
> PFA v18: I've added a test for Nik's case and for COMMIT AND CHAIN. Now we need to fix stuff to pass this tests (I've crafted output).
> We also need test for patchset step "Try to enable transaction_timeout before next command".
>
> Thanks!

After exploring the code, I found scheduling the timeout in
`StartTransaction` might be a reasonable idea, all the chain
commands will call this function.

What concerns me is that it is also called by StartParallelWorkerTransaction,
I'm not sure if we should enable this timeout for parallel execution.

Thought?

>
>
> Best regards, Andrey Borodin.



-- 
Regards
Junwang Zhao


Attachments:

  [application/octet-stream] v19-0002-Use-test-from-Li-Japin-Also-add-tests-for-multip.patch (8.9K, ../../CAEG8a3J6mSPXgHq_aqC7yk4xq1e9XF9B1Q+QVBr48fzdw1uKqw@mail.gmail.com/2-v19-0002-Use-test-from-Li-Japin-Also-add-tests-for-multip.patch)
  download | inline diff:
From 9854b0d6ed1947b13d7d1206860d992575de5257 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Fri, 29 Dec 2023 14:54:02 +0500
Subject: [PATCH v19 2/4] Use test from Li Japin Also add tests for multiple
 queries in transaction and COMMIT AND CHAIN.

---
 src/test/isolation/expected/timeouts.out | 97 +++++++++++++++++++-----
 src/test/isolation/specs/timeouts.spec   | 64 ++++++++++------
 2 files changed, 119 insertions(+), 42 deletions(-)

diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index cabe28f2c8..9fb371baf1 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 6 sessions
+Parsed test spec with 7 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -80,39 +80,98 @@ step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
 
-starting permutation: stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4
-step stt1_set: SET transaction_timeout = '1ms';
-step stt1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_here: SELECT pg_sleep(1);
-FATAL:  terminating connection due to transaction timeout
-server closed the connection unexpectedly
-	This probably means the server terminated abnormally
-	before or while processing the request.
-
-step stt2_set: SET transaction_timeout = '1ms';
-step stt2_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_there: SELECT pg_sleep(0.1);
+starting permutation: stto s3_begin sleep s3_check abort
+step stto: SET statement_timeout = '1ms'; SET transaction_timeout = '1s';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep: SELECT pg_sleep(0.1);
+ERROR:  canceling statement due to statement timeout
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    1
+(1 row)
+
+step abort: ABORT;
+
+starting permutation: tsto s3_begin wait_check s3_check
+step tsto: SET statement_timeout = '1s'; SET transaction_timeout = '1ms';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step wait_check: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: itto s4_begin wait_check s4_check
+step itto: SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s';
+step s4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step wait_check: SELECT pg_sleep(0.1);
 pg_sleep
 --------
         
 (1 row)
 
-step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
+step s4_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4';
 count
 -----
     0
 (1 row)
 
-step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
-step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_there: SELECT pg_sleep(0.1);
+
+starting permutation: tito s5_begin wait_check s5_check
+step tito: SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms';
+step s5_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step wait_check: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s5_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_select_1 wait_check s7_check
+step s7_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '150ms';
+
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_commit_and_chain: COMMIT AND CHAIN;
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_select_1: SELECT 1;
+?column?
+--------
+       1
+(1 row)
+
+step wait_check: SELECT pg_sleep(0.1);
 pg_sleep
 --------
         
 (1 row)
 
-step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
-step stt3_check_itt4: <... completed>
+step s7_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7';
 count
 -----
     0
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index 2772939b6b..5c353d4c61 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -18,7 +18,7 @@ step wrtbl	{ UPDATE accounts SET balance = balance + 100; }
 teardown	{ ABORT; }
 
 session s2
-setup		{ SET transaction_timeout = '10s'; SET idle_in_transaction_session_timeout = '10s'; BEGIN ISOLATION LEVEL READ COMMITTED; }
+setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
 step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
@@ -27,28 +27,38 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
-session stt1
-# enable statement_timeout to check interaction
-setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
-step stt1_set	{ SET transaction_timeout = '1ms'; }
-step stt1_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
-step sleep_here	{ SELECT pg_sleep(1); }
+session s3
+step s3_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step stto	{ SET statement_timeout = '1ms'; SET transaction_timeout = '1s'; }
+step tsto	{ SET statement_timeout = '1s'; SET transaction_timeout = '1ms'; }
+step sleep	{ SELECT pg_sleep(0.1); }
+step abort	{ ABORT; }
+
+session s4
+step s4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step itto	{ SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s'; }
 
-session stt2
-setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
-step stt2_set	{ SET transaction_timeout = '1ms'; }
-step stt2_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
-# Session stt2 is terminated in the background. However, isolation tester needs a step to observe it.
+session s5
+step s5_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step tito	{ SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms'; }
 
-session stt3
-step sleep_there{ SELECT pg_sleep(0.1); }
-# Observe that stt2\itt4 died
-step stt3_check_stt2 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2' }
-step stt3_check_itt4 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' }
+session s6
+step wait_check	{ SELECT pg_sleep(0.1); }
+step s3_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3'; }
+step s4_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4'; }
+step s5_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5'; }
+step s7_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7'; }
 
-session itt4
-step itt4_set	{ SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s'; }
-step itt4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+session s7
+step s7_begin
+{
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '150ms';
+}
+step s7_commit_and_chain { COMMIT AND CHAIN; }
+# to test that quick query does not restart transaction_timeout
+step s7_select_1 { SELECT 1; }
+step s7_sleep	{ SELECT pg_sleep(0.1); }
 
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
@@ -71,6 +81,14 @@ permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
 
-# timeout of active query, idle transaction timeout
-permutation stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4(*)
-# can't run tests after this, sessions stt1, stt2, and itt4 are expected to FATAL-out
+# statement timeout expires first
+permutation stto s3_begin sleep s3_check abort
+# transaction timeout expires first, session s3 FATAL-out
+permutation tsto s3_begin wait_check s3_check
+# idle in transaction timeout expires first, session s4 FATAL-out
+permutation itto s4_begin wait_check s4_check
+# transaction timeout expires first, session s5 FATAL-out
+permutation tito s5_begin wait_check s5_check
+# transaction timeout expires in presence of query flow
+# session s7 FATAL-out sleeping in last wait_check only
+permutation s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_select_1 wait_check s7_check
-- 
2.41.0



  [application/octet-stream] v19-0003-Try-to-enable-transaction_timeout-before-next-co.patch (1.4K, ../../CAEG8a3J6mSPXgHq_aqC7yk4xq1e9XF9B1Q+QVBr48fzdw1uKqw@mail.gmail.com/3-v19-0003-Try-to-enable-transaction_timeout-before-next-co.patch)
  download | inline diff:
From 6cb4eced7ac0c6a6dee995f0a0057c3abe8d42af Mon Sep 17 00:00:00 2001
From: japinli <[email protected]>
Date: Sat, 23 Dec 2023 11:04:25 +0800
Subject: [PATCH v19 3/4] Try to enable transaction_timeout before next command

---
 src/backend/tcop/postgres.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index a2611cf8e6..96161eb7ab 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -4513,6 +4513,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else if (IsTransactionOrTransactionBlock())
 			{
@@ -4527,6 +4532,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else
 			{
-- 
2.41.0



  [application/octet-stream] v19-0001-Introduce-transaction_timeout.patch (19.8K, ../../CAEG8a3J6mSPXgHq_aqC7yk4xq1e9XF9B1Q+QVBr48fzdw1uKqw@mail.gmail.com/4-v19-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From 812e6af774b1ecce2d5bbf6c280d938a75f1fe16 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v19 1/4] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>
Reviewed-by: Japin Li <[email protected]>
Reviewed-by: Junwang Zhao <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 35 ++++++++++++++++
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 27 ++++++++++--
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 +++++
 src/backend/utils/misc/guc_tables.c           | 11 +++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/expected/timeouts.out      | 41 ++++++++++++++++++-
 src/test/isolation/specs/timeouts.spec        | 29 ++++++++++++-
 17 files changed, 162 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index b5624ca884..d62edcf83b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9134,6 +9134,41 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index b04fcfc8c8..e6fa1cfdc2 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b6451d9d08..4be06c1e5d 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7298a187d1..a2611cf8e6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,6 +2745,10 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		/* Schedule or reschedule transaction timeout */
+		if (TransactionTimeout > 0)
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -3426,6 +3430,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,7 +4506,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4504,7 +4520,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4562,6 +4579,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5140,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 8e97a0150f..8f1157afee 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 60bc1217fb..fd586c193c 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 552cf9d950..64be4de0c7 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 9f59440526..c5cfbef02b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2556,6 +2556,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index b2809c711a..0b37117eb7 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -700,6 +700,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4..3342971bd0 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8c0b5486b9..21bd16ef00 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1251,6 +1251,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 417c74cfef..9cda3f3667 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 74bc2f97cb..b2d0f84252 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index e87fd25d64..9dde9cbfdd 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -429,6 +429,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 8a61853371..608a83d5a8 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..cabe28f2c8 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 6 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,42 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4
+step stt1_set: SET transaction_timeout = '1ms';
+step stt1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_here: SELECT pg_sleep(1);
+FATAL:  terminating connection due to transaction timeout
+server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+
+step stt2_set: SET transaction_timeout = '1ms';
+step stt2_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
+count
+-----
+    0
+(1 row)
+
+step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
+step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
+step stt3_check_itt4: <... completed>
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..2772939b6b 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -18,7 +18,7 @@ step wrtbl	{ UPDATE accounts SET balance = balance + 100; }
 teardown	{ ABORT; }
 
 session s2
-setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+setup		{ SET transaction_timeout = '10s'; SET idle_in_transaction_session_timeout = '10s'; BEGIN ISOLATION LEVEL READ COMMITTED; }
 step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
@@ -27,6 +27,29 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session stt1
+# enable statement_timeout to check interaction
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt1_set	{ SET transaction_timeout = '1ms'; }
+step stt1_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step sleep_here	{ SELECT pg_sleep(1); }
+
+session stt2
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt2_set	{ SET transaction_timeout = '1ms'; }
+step stt2_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+# Session stt2 is terminated in the background. However, isolation tester needs a step to observe it.
+
+session stt3
+step sleep_there{ SELECT pg_sleep(0.1); }
+# Observe that stt2\itt4 died
+step stt3_check_stt2 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2' }
+step stt3_check_itt4 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' }
+
+session itt4
+step itt4_set	{ SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s'; }
+step itt4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +70,7 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# timeout of active query, idle transaction timeout
+permutation stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4(*)
+# can't run tests after this, sessions stt1, stt2, and itt4 are expected to FATAL-out
-- 
2.41.0



  [application/octet-stream] v19-0004-fix-reschedule-timeout-for-each-commmand.patch (1.3K, ../../CAEG8a3J6mSPXgHq_aqC7yk4xq1e9XF9B1Q+QVBr48fzdw1uKqw@mail.gmail.com/5-v19-0004-fix-reschedule-timeout-for-each-commmand.patch)
  download | inline diff:
From 9e5d261b17e395a87b7bdcd4555d6b4bf49c844e Mon Sep 17 00:00:00 2001
From: Zhao Junwang <[email protected]>
Date: Fri, 29 Dec 2023 18:41:24 +0800
Subject: [PATCH v19 4/4] fix reschedule timeout for each commmand

Signed-off-by: Zhao Junwang <[email protected]>
---
 src/backend/access/transam/xact.c | 4 ++++
 src/backend/tcop/postgres.c       | 4 ----
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 8442c5e6a7..2d9b718762 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2139,6 +2139,10 @@ StartTransaction(void)
 	 */
 	s->state = TRANS_INPROGRESS;
 
+	/* Schedule transaction timeout */
+	if (TransactionTimeout > 0)
+		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 96161eb7ab..36b9e3f8c5 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,10 +2745,6 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
-		/* Schedule or reschedule transaction timeout */
-		if (TransactionTimeout > 0)
-			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
-
 		xact_started = true;
 	}
 
-- 
2.41.0



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2023-12-29 11:15  Andrey M. Borodin <[email protected]>
  parent: Junwang Zhao <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2023-12-29 11:15 UTC (permalink / raw)
  To: Junwang Zhao <[email protected]>; +Cc: Li Japin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>



> On 29 Dec 2023, at 16:00, Junwang Zhao <[email protected]> wrote:
> 
> After exploring the code, I found scheduling the timeout in
> `StartTransaction` might be a reasonable idea, all the chain
> commands will call this function.
> 
> What concerns me is that it is also called by StartParallelWorkerTransaction,
> I'm not sure if we should enable this timeout for parallel execution.

I think for parallel workers we should mimic statement_timeout. Because these workers have per-statemenent lifetime.


Best regards, Andrey Borodin.





^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-01-01 14:28  Andrey M. Borodin <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2024-01-01 14:28 UTC (permalink / raw)
  To: Junwang Zhao <[email protected]>; +Cc: Li Japin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>



> On 29 Dec 2023, at 16:15, Andrey M. Borodin <[email protected]> wrote:

PFA v20. Code steps are intact.

Further refactored tests:
    1. Check termination of active and idle queries (previously tests from Li were testing only termination of idle query)
    2. Check timeout reschedule (even when last active query was 'SET transaction_timeout')
    3. Check that timeout is not rescheduled by new queries (Nik's case)


Do we have any other open items?
I've left 'make check-timeouts' in isolation directory, it's for development purposes. I think we should remove this before committing. Obviously, all patch steps are expected to be squashed before commit.


Best regards, Andrey Borodin.


Attachments:

  [application/octet-stream] v20-0001-Introduce-transaction_timeout.patch (19.8K, ../../[email protected]/2-v20-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From d9f6e4d7c7183fe6042f11d98270f707f87b9e97 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v20 1/4] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>
Reviewed-by: Japin Li <[email protected]>
Reviewed-by: Junwang Zhao <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 35 ++++++++++++++++
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 27 ++++++++++--
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 +++++
 src/backend/utils/misc/guc_tables.c           | 11 +++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/expected/timeouts.out      | 41 ++++++++++++++++++-
 src/test/isolation/specs/timeouts.spec        | 29 ++++++++++++-
 17 files changed, 162 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f323bba018..0d849a11ce 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9134,6 +9134,41 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index b04fcfc8c8..e6fa1cfdc2 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b6451d9d08..4be06c1e5d 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7298a187d1..a2611cf8e6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,6 +2745,10 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		/* Schedule or reschedule transaction timeout */
+		if (TransactionTimeout > 0)
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -3426,6 +3430,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,7 +4506,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4504,7 +4520,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4562,6 +4579,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5140,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 8e97a0150f..8f1157afee 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 60bc1217fb..fd586c193c 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 552cf9d950..64be4de0c7 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 3945a92ddd..fcb214a04d 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2567,6 +2567,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index b2809c711a..0b37117eb7 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -700,6 +700,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4..3342971bd0 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 050a831226..39ca7e6d38 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1251,6 +1251,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 417c74cfef..9cda3f3667 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 74bc2f97cb..b2d0f84252 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index e87fd25d64..9dde9cbfdd 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -429,6 +429,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 8a61853371..608a83d5a8 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..cabe28f2c8 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 6 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,42 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4
+step stt1_set: SET transaction_timeout = '1ms';
+step stt1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_here: SELECT pg_sleep(1);
+FATAL:  terminating connection due to transaction timeout
+server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+
+step stt2_set: SET transaction_timeout = '1ms';
+step stt2_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
+count
+-----
+    0
+(1 row)
+
+step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
+step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
+step stt3_check_itt4: <... completed>
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..2772939b6b 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -18,7 +18,7 @@ step wrtbl	{ UPDATE accounts SET balance = balance + 100; }
 teardown	{ ABORT; }
 
 session s2
-setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+setup		{ SET transaction_timeout = '10s'; SET idle_in_transaction_session_timeout = '10s'; BEGIN ISOLATION LEVEL READ COMMITTED; }
 step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
@@ -27,6 +27,29 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session stt1
+# enable statement_timeout to check interaction
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt1_set	{ SET transaction_timeout = '1ms'; }
+step stt1_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step sleep_here	{ SELECT pg_sleep(1); }
+
+session stt2
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt2_set	{ SET transaction_timeout = '1ms'; }
+step stt2_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+# Session stt2 is terminated in the background. However, isolation tester needs a step to observe it.
+
+session stt3
+step sleep_there{ SELECT pg_sleep(0.1); }
+# Observe that stt2\itt4 died
+step stt3_check_stt2 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2' }
+step stt3_check_itt4 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' }
+
+session itt4
+step itt4_set	{ SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s'; }
+step itt4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +70,7 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# timeout of active query, idle transaction timeout
+permutation stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4(*)
+# can't run tests after this, sessions stt1, stt2, and itt4 are expected to FATAL-out
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v20-0004-fix-reschedule-timeout-for-each-commmand.patch (1.3K, ../../[email protected]/3-v20-0004-fix-reschedule-timeout-for-each-commmand.patch)
  download | inline diff:
From c640a812bf272e4566545dd31168e5c63bb574af Mon Sep 17 00:00:00 2001
From: Zhao Junwang <[email protected]>
Date: Fri, 29 Dec 2023 18:41:24 +0800
Subject: [PATCH v20 4/4] fix reschedule timeout for each commmand

Signed-off-by: Zhao Junwang <[email protected]>
---
 src/backend/access/transam/xact.c | 4 ++++
 src/backend/tcop/postgres.c       | 4 ----
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 8442c5e6a7..2d9b718762 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2139,6 +2139,10 @@ StartTransaction(void)
 	 */
 	s->state = TRANS_INPROGRESS;
 
+	/* Schedule transaction timeout */
+	if (TransactionTimeout > 0)
+		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 96161eb7ab..36b9e3f8c5 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,10 +2745,6 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
-		/* Schedule or reschedule transaction timeout */
-		if (TransactionTimeout > 0)
-			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
-
 		xact_started = true;
 	}
 
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v20-0003-Try-to-enable-transaction_timeout-before-next-co.patch (1.4K, ../../[email protected]/4-v20-0003-Try-to-enable-transaction_timeout-before-next-co.patch)
  download | inline diff:
From 50c197cdd19b83fb804be0332c3667d811bfbec9 Mon Sep 17 00:00:00 2001
From: japinli <[email protected]>
Date: Sat, 23 Dec 2023 11:04:25 +0800
Subject: [PATCH v20 3/4] Try to enable transaction_timeout before next command

---
 src/backend/tcop/postgres.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index a2611cf8e6..96161eb7ab 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -4513,6 +4513,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else if (IsTransactionOrTransactionBlock())
 			{
@@ -4527,6 +4532,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else
 			{
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v20-0002-Add-better-tests-for-transaction_timeout.patch (10.4K, ../../[email protected]/5-v20-0002-Add-better-tests-for-transaction_timeout.patch)
  download | inline diff:
From cb06a90d28f36e314d53db8cca4d6c1030ff56cd Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Fri, 29 Dec 2023 14:54:02 +0500
Subject: [PATCH v20 2/4] Add better tests for transaction_timeout: 1. Check
 COMMIT AND CHAIN 2. Check termination of active and idle queries 3. Check
 timeout reschedult 4. Check that timeout is not rescheduled by new queries

---
 src/test/isolation/Makefile              |   3 +
 src/test/isolation/expected/timeouts.out | 104 ++++++++++++++++++++---
 src/test/isolation/specs/timeouts.spec   |  72 +++++++++++-----
 3 files changed, 142 insertions(+), 37 deletions(-)

diff --git a/src/test/isolation/Makefile b/src/test/isolation/Makefile
index ade2256ed3..482bb31949 100644
--- a/src/test/isolation/Makefile
+++ b/src/test/isolation/Makefile
@@ -72,3 +72,6 @@ installcheck-prepared-txns: all temp-install
 
 check-prepared-txns: all temp-install
 	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule prepared-transactions prepared-transactions-cic
+
+check-timeouts: all temp-install
+	$(pg_isolation_regress_check) timeouts
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index cabe28f2c8..4db7cb38d5 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,5 @@
-Parsed test spec with 6 sessions
+unused step name: s6_sleep
+Parsed test spec with 8 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -80,39 +81,114 @@ step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
 
-starting permutation: stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4
-step stt1_set: SET transaction_timeout = '1ms';
-step stt1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_here: SELECT pg_sleep(1);
+starting permutation: stto s3_begin s3_sleep s3_check abort
+step stto: SET statement_timeout = '1ms'; SET transaction_timeout = '1s';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s3_sleep: SELECT pg_sleep(0.1);
+ERROR:  canceling statement due to statement timeout
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    1
+(1 row)
+
+step abort: ABORT;
+
+starting permutation: tsto s3_begin s3_sleep s3_check
+step tsto: SET statement_timeout = '1s'; SET transaction_timeout = '1ms';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s3_sleep: SELECT pg_sleep(0.1);
 FATAL:  terminating connection due to transaction timeout
 server closed the connection unexpectedly
 	This probably means the server terminated abnormally
 	before or while processing the request.
 
-step stt2_set: SET transaction_timeout = '1ms';
-step stt2_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_there: SELECT pg_sleep(0.1);
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: itto s4_begin checker_sleep s4_check
+step itto: SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s';
+step s4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
 pg_sleep
 --------
         
 (1 row)
 
-step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
+step s4_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4';
 count
 -----
     0
 (1 row)
 
-step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
-step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_there: SELECT pg_sleep(0.1);
+
+starting permutation: tito s5_begin checker_sleep s5_check
+step tito: SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms';
+step s5_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s5_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: s6_begin s6_tt checker_sleep s6_check
+step s6_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s6_tt: SET statement_timeout = '1s'; SET transaction_timeout = '1ms';
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s6_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_select_1 checker_sleep s7_check
+step s7_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '150ms';
+
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_commit_and_chain: COMMIT AND CHAIN;
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_select_1: SELECT 1;
+?column?
+--------
+       1
+(1 row)
+
+step checker_sleep: SELECT pg_sleep(0.1);
 pg_sleep
 --------
         
 (1 row)
 
-step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
-step stt3_check_itt4: <... completed>
+step s7_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7';
 count
 -----
     0
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index 2772939b6b..e778256b16 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -18,7 +18,7 @@ step wrtbl	{ UPDATE accounts SET balance = balance + 100; }
 teardown	{ ABORT; }
 
 session s2
-setup		{ SET transaction_timeout = '10s'; SET idle_in_transaction_session_timeout = '10s'; BEGIN ISOLATION LEVEL READ COMMITTED; }
+setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
 step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
@@ -27,28 +27,44 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
-session stt1
-# enable statement_timeout to check interaction
-setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
-step stt1_set	{ SET transaction_timeout = '1ms'; }
-step stt1_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
-step sleep_here	{ SELECT pg_sleep(1); }
+session s3
+step s3_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step stto	{ SET statement_timeout = '1ms'; SET transaction_timeout = '1s'; }
+step tsto	{ SET statement_timeout = '1s'; SET transaction_timeout = '1ms'; }
+step s3_sleep	{ SELECT pg_sleep(0.1); }
+step abort	{ ABORT; }
+
+session s4
+step s4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step itto	{ SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s'; }
+
+session s5
+step s5_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step tito	{ SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms'; }
 
-session stt2
-setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
-step stt2_set	{ SET transaction_timeout = '1ms'; }
-step stt2_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
-# Session stt2 is terminated in the background. However, isolation tester needs a step to observe it.
+session s6
+step s6_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step s6_tt	{ SET statement_timeout = '1s'; SET transaction_timeout = '1ms'; }
+step s6_sleep	{ SELECT pg_sleep(0.1); }
 
-session stt3
-step sleep_there{ SELECT pg_sleep(0.1); }
-# Observe that stt2\itt4 died
-step stt3_check_stt2 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2' }
-step stt3_check_itt4 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' }
+session s7
+step s7_begin
+{
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '150ms';
+}
+step s7_commit_and_chain { COMMIT AND CHAIN; }
+# to test that quick query does not restart transaction_timeout
+step s7_select_1 { SELECT 1; }
+step s7_sleep	{ SELECT pg_sleep(0.1); }
 
-session itt4
-step itt4_set	{ SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s'; }
-step itt4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+session checker
+step checker_sleep	{ SELECT pg_sleep(0.1); }
+step s3_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3'; }
+step s4_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4'; }
+step s5_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5'; }
+step s6_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6'; }
+step s7_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7'; }
 
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
@@ -71,6 +87,16 @@ permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
 
-# timeout of active query, idle transaction timeout
-permutation stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4(*)
-# can't run tests after this, sessions stt1, stt2, and itt4 are expected to FATAL-out
+# statement timeout expires first
+permutation stto s3_begin s3_sleep s3_check abort
+# transaction timeout expires first, session s3 FATAL-out
+permutation tsto s3_begin s3_sleep s3_check
+# idle in transaction timeout expires first, session s4 FATAL-out
+permutation itto s4_begin checker_sleep s4_check
+# transaction timeout expires first, session s5 FATAL-out
+permutation tito s5_begin checker_sleep s5_check
+# transaction timeout can be schedule amid transaction, session s6 FATAL-out
+permutation s6_begin s6_tt checker_sleep s6_check
+# transaction timeout expires in presence of query flow
+# session s7 FATAL-out sleeping in last checker_sleep only
+permutation s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_select_1 checker_sleep s7_check
-- 
2.37.1 (Apple Git-137.1)



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-01-03 06:39  Andrey M. Borodin <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2024-01-03 06:39 UTC (permalink / raw)
  To: Junwang Zhao <[email protected]>; +Cc: Li Japin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>



> On 1 Jan 2024, at 19:28, Andrey M. Borodin <[email protected]> wrote:
> 
>  3. Check that timeout is not rescheduled by new queries (Nik's case)

The test of Nik's case was not stable enough together with COMMIT AND CHAIN. So I've separated these cases into different permutations.
Looking through CI logs it seems variation in sleeps and actual timeouts easily reach 30+ms. I'm not entirely sure we can reach 100% stable tests without too big timeouts.


Best regards, Andrey Borodin.






Attachments:

  [application/octet-stream] v21-0001-Introduce-transaction_timeout.patch (19.8K, ../../[email protected]/3-v21-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From d9f6e4d7c7183fe6042f11d98270f707f87b9e97 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v21 1/4] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>
Reviewed-by: Japin Li <[email protected]>
Reviewed-by: Junwang Zhao <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 35 ++++++++++++++++
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 27 ++++++++++--
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 +++++
 src/backend/utils/misc/guc_tables.c           | 11 +++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/expected/timeouts.out      | 41 ++++++++++++++++++-
 src/test/isolation/specs/timeouts.spec        | 29 ++++++++++++-
 17 files changed, 162 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f323bba018..0d849a11ce 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9134,6 +9134,41 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index b04fcfc8c8..e6fa1cfdc2 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b6451d9d08..4be06c1e5d 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7298a187d1..a2611cf8e6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,6 +2745,10 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		/* Schedule or reschedule transaction timeout */
+		if (TransactionTimeout > 0)
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -3426,6 +3430,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,7 +4506,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4504,7 +4520,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4562,6 +4579,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5140,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 8e97a0150f..8f1157afee 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 60bc1217fb..fd586c193c 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 552cf9d950..64be4de0c7 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 3945a92ddd..fcb214a04d 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2567,6 +2567,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index b2809c711a..0b37117eb7 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -700,6 +700,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4..3342971bd0 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 050a831226..39ca7e6d38 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1251,6 +1251,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 417c74cfef..9cda3f3667 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 74bc2f97cb..b2d0f84252 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index e87fd25d64..9dde9cbfdd 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -429,6 +429,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 8a61853371..608a83d5a8 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..cabe28f2c8 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 6 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,42 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4
+step stt1_set: SET transaction_timeout = '1ms';
+step stt1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_here: SELECT pg_sleep(1);
+FATAL:  terminating connection due to transaction timeout
+server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+
+step stt2_set: SET transaction_timeout = '1ms';
+step stt2_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
+count
+-----
+    0
+(1 row)
+
+step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
+step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
+step stt3_check_itt4: <... completed>
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..2772939b6b 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -18,7 +18,7 @@ step wrtbl	{ UPDATE accounts SET balance = balance + 100; }
 teardown	{ ABORT; }
 
 session s2
-setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+setup		{ SET transaction_timeout = '10s'; SET idle_in_transaction_session_timeout = '10s'; BEGIN ISOLATION LEVEL READ COMMITTED; }
 step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
@@ -27,6 +27,29 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session stt1
+# enable statement_timeout to check interaction
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt1_set	{ SET transaction_timeout = '1ms'; }
+step stt1_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step sleep_here	{ SELECT pg_sleep(1); }
+
+session stt2
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt2_set	{ SET transaction_timeout = '1ms'; }
+step stt2_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+# Session stt2 is terminated in the background. However, isolation tester needs a step to observe it.
+
+session stt3
+step sleep_there{ SELECT pg_sleep(0.1); }
+# Observe that stt2\itt4 died
+step stt3_check_stt2 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2' }
+step stt3_check_itt4 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' }
+
+session itt4
+step itt4_set	{ SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s'; }
+step itt4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +70,7 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# timeout of active query, idle transaction timeout
+permutation stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4(*)
+# can't run tests after this, sessions stt1, stt2, and itt4 are expected to FATAL-out
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v21-0002-Add-better-tests-for-transaction_timeout.patch (10.9K, ../../[email protected]/5-v21-0002-Add-better-tests-for-transaction_timeout.patch)
  download | inline diff:
From 1ed73bb8ee489483aac5522b417815d743141cc0 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Fri, 29 Dec 2023 14:54:02 +0500
Subject: [PATCH v21 2/4] Add better tests for transaction_timeout: 1. Check
 COMMIT AND CHAIN 2. Check termination of active and idle queries 3. Check
 timeout reschedult 4. Check that timeout is not rescheduled by new queries

---
 src/test/isolation/Makefile              |   3 +
 src/test/isolation/expected/timeouts.out | 123 ++++++++++++++++++++---
 src/test/isolation/specs/timeouts.spec   |  74 +++++++++-----
 3 files changed, 163 insertions(+), 37 deletions(-)

diff --git a/src/test/isolation/Makefile b/src/test/isolation/Makefile
index ade2256ed3..482bb31949 100644
--- a/src/test/isolation/Makefile
+++ b/src/test/isolation/Makefile
@@ -72,3 +72,6 @@ installcheck-prepared-txns: all temp-install
 
 check-prepared-txns: all temp-install
 	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule prepared-transactions prepared-transactions-cic
+
+check-timeouts: all temp-install
+	$(pg_isolation_regress_check) timeouts
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index cabe28f2c8..a500e9ab91 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,5 @@
-Parsed test spec with 6 sessions
+unused step name: s6_sleep
+Parsed test spec with 8 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -80,39 +81,133 @@ step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
 
-starting permutation: stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4
-step stt1_set: SET transaction_timeout = '1ms';
-step stt1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_here: SELECT pg_sleep(1);
+starting permutation: stto s3_begin s3_sleep s3_check s3_abort
+step stto: SET statement_timeout = '1ms'; SET transaction_timeout = '1s';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s3_sleep: SELECT pg_sleep(0.1);
+ERROR:  canceling statement due to statement timeout
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    1
+(1 row)
+
+step s3_abort: ABORT;
+
+starting permutation: tsto s3_begin s3_sleep s3_check
+step tsto: SET statement_timeout = '1s'; SET transaction_timeout = '1ms';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s3_sleep: SELECT pg_sleep(0.1);
 FATAL:  terminating connection due to transaction timeout
 server closed the connection unexpectedly
 	This probably means the server terminated abnormally
 	before or while processing the request.
 
-step stt2_set: SET transaction_timeout = '1ms';
-step stt2_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_there: SELECT pg_sleep(0.1);
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: itto s4_begin checker_sleep s4_check
+step itto: SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s';
+step s4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s4_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: tito s5_begin checker_sleep s5_check
+step tito: SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms';
+step s5_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s5_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: s6_begin s6_tt checker_sleep s6_check
+step s6_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s6_tt: SET statement_timeout = '1s'; SET transaction_timeout = '1ms';
+step checker_sleep: SELECT pg_sleep(0.1);
 pg_sleep
 --------
         
 (1 row)
 
-step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
+step s6_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6';
 count
 -----
     0
 (1 row)
 
-step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
-step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_there: SELECT pg_sleep(0.1);
+
+starting permutation: s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_check s7_abort
+step s7_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '150ms';
+
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_commit_and_chain: COMMIT AND CHAIN;
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7';
+count
+-----
+    1
+(1 row)
+
+step s7_abort: ABORT;
+
+starting permutation: s7_begin s7_sleep s7_select_1 checker_sleep s7_check
+step s7_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '150ms';
+
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_select_1: SELECT 1;
+?column?
+--------
+       1
+(1 row)
+
+step checker_sleep: SELECT pg_sleep(0.1);
 pg_sleep
 --------
         
 (1 row)
 
-step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
-step stt3_check_itt4: <... completed>
+step s7_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7';
 count
 -----
     0
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index 2772939b6b..dc47d4f362 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -18,7 +18,7 @@ step wrtbl	{ UPDATE accounts SET balance = balance + 100; }
 teardown	{ ABORT; }
 
 session s2
-setup		{ SET transaction_timeout = '10s'; SET idle_in_transaction_session_timeout = '10s'; BEGIN ISOLATION LEVEL READ COMMITTED; }
+setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
 step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
@@ -27,28 +27,45 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
-session stt1
-# enable statement_timeout to check interaction
-setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
-step stt1_set	{ SET transaction_timeout = '1ms'; }
-step stt1_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
-step sleep_here	{ SELECT pg_sleep(1); }
+session s3
+step s3_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step stto	{ SET statement_timeout = '1ms'; SET transaction_timeout = '1s'; }
+step tsto	{ SET statement_timeout = '1s'; SET transaction_timeout = '1ms'; }
+step s3_sleep	{ SELECT pg_sleep(0.1); }
+step s3_abort	{ ABORT; }
+
+session s4
+step s4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step itto	{ SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s'; }
+
+session s5
+step s5_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step tito	{ SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms'; }
 
-session stt2
-setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
-step stt2_set	{ SET transaction_timeout = '1ms'; }
-step stt2_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
-# Session stt2 is terminated in the background. However, isolation tester needs a step to observe it.
+session s6
+step s6_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step s6_tt	{ SET statement_timeout = '1s'; SET transaction_timeout = '1ms'; }
+step s6_sleep	{ SELECT pg_sleep(0.1); }
 
-session stt3
-step sleep_there{ SELECT pg_sleep(0.1); }
-# Observe that stt2\itt4 died
-step stt3_check_stt2 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2' }
-step stt3_check_itt4 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' }
+session s7
+step s7_begin
+{
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '150ms';
+}
+step s7_commit_and_chain { COMMIT AND CHAIN; }
+# to test that quick query does not restart transaction_timeout
+step s7_select_1 { SELECT 1; }
+step s7_sleep	{ SELECT pg_sleep(0.1); }
+step s7_abort	{ ABORT; }
 
-session itt4
-step itt4_set	{ SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s'; }
-step itt4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+session checker
+step checker_sleep	{ SELECT pg_sleep(0.1); }
+step s3_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3'; }
+step s4_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4'; }
+step s5_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5'; }
+step s6_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6'; }
+step s7_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7'; }
 
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
@@ -71,6 +88,17 @@ permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
 
-# timeout of active query, idle transaction timeout
-permutation stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4(*)
-# can't run tests after this, sessions stt1, stt2, and itt4 are expected to FATAL-out
+# statement timeout expires first
+permutation stto s3_begin s3_sleep s3_check s3_abort
+# transaction timeout expires first, session s3 FATAL-out
+permutation tsto s3_begin s3_sleep s3_check
+# idle in transaction timeout expires first, session s4 FATAL-out
+permutation itto s4_begin checker_sleep s4_check
+# transaction timeout expires first, session s5 FATAL-out
+permutation tito s5_begin checker_sleep s5_check
+# transaction timeout can be schedule amid transaction, session s6 FATAL-out
+permutation s6_begin s6_tt checker_sleep s6_check
+# COMMIT AND CHAIN must restart transaction timeout
+permutation s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_check s7_abort
+# transaction timeout expires in presence of query flow, session s7 FATAL-out
+permutation s7_begin s7_sleep s7_select_1 checker_sleep s7_check
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v21-0003-Try-to-enable-transaction_timeout-before-next-co.patch (1.4K, ../../[email protected]/7-v21-0003-Try-to-enable-transaction_timeout-before-next-co.patch)
  download | inline diff:
From 86b4d6f4df132da83fc4d754b54eaefe17c303ee Mon Sep 17 00:00:00 2001
From: japinli <[email protected]>
Date: Sat, 23 Dec 2023 11:04:25 +0800
Subject: [PATCH v21 3/4] Try to enable transaction_timeout before next command

---
 src/backend/tcop/postgres.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index a2611cf8e6..96161eb7ab 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -4513,6 +4513,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else if (IsTransactionOrTransactionBlock())
 			{
@@ -4527,6 +4532,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else
 			{
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v21-0004-fix-reschedule-timeout-for-each-commmand.patch (1.3K, ../../[email protected]/9-v21-0004-fix-reschedule-timeout-for-each-commmand.patch)
  download | inline diff:
From f7d67812410678c8f547c1f2038817f4426f0516 Mon Sep 17 00:00:00 2001
From: Zhao Junwang <[email protected]>
Date: Fri, 29 Dec 2023 18:41:24 +0800
Subject: [PATCH v21 4/4] fix reschedule timeout for each commmand

Signed-off-by: Zhao Junwang <[email protected]>
---
 src/backend/access/transam/xact.c | 4 ++++
 src/backend/tcop/postgres.c       | 4 ----
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 8442c5e6a7..2d9b718762 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2139,6 +2139,10 @@ StartTransaction(void)
 	 */
 	s->state = TRANS_INPROGRESS;
 
+	/* Schedule transaction timeout */
+	if (TransactionTimeout > 0)
+		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 96161eb7ab..36b9e3f8c5 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,10 +2745,6 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
-		/* Schedule or reschedule transaction timeout */
-		if (TransactionTimeout > 0)
-			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
-
 		xact_started = true;
 	}
 
-- 
2.37.1 (Apple Git-137.1)



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-01-03 11:46  Andrey M. Borodin <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2024-01-03 11:46 UTC (permalink / raw)
  To: Junwang Zhao <[email protected]>; +Cc: Li Japin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>



> On 3 Jan 2024, at 11:39, Andrey M. Borodin <[email protected]> wrote:
> 
> 
> 
>> On 1 Jan 2024, at 19:28, Andrey M. Borodin <[email protected] <mailto:[email protected]>> wrote:
>> 
>>  3. Check that timeout is not rescheduled by new queries (Nik's case)
> 
> The test of Nik's case was not stable enough together with COMMIT AND CHAIN. So I've separated these cases into different permutations.
> Looking through CI logs it seems variation in sleeps and actual timeouts easily reach 30+ms. I'm not entirely sure we can reach 100% stable tests without too big timeouts.
> 
> 
> Best regards, Andrey Borodin.
> 
> <v21-0001-Introduce-transaction_timeout.patch>
> <v21-0002-Add-better-tests-for-transaction_timeout.patch>
> <v21-0003-Try-to-enable-transaction_timeout-before-next-co.patch>
> <v21-0004-fix-reschedule-timeout-for-each-commmand.patch>
 I do not understand why, but mailing list did not pick patches that I sent. I'll retry.


Best regards, Andrey Borodin.



Attachments:

  [application/octet-stream] v21-0001-Introduce-transaction_timeout.patch (19.8K, ../../[email protected]/3-v21-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From d9f6e4d7c7183fe6042f11d98270f707f87b9e97 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v21 1/4] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>
Reviewed-by: Japin Li <[email protected]>
Reviewed-by: Junwang Zhao <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 35 ++++++++++++++++
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 27 ++++++++++--
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 +++++
 src/backend/utils/misc/guc_tables.c           | 11 +++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/expected/timeouts.out      | 41 ++++++++++++++++++-
 src/test/isolation/specs/timeouts.spec        | 29 ++++++++++++-
 17 files changed, 162 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f323bba018..0d849a11ce 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9134,6 +9134,41 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index b04fcfc8c8..e6fa1cfdc2 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b6451d9d08..4be06c1e5d 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7298a187d1..a2611cf8e6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,6 +2745,10 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		/* Schedule or reschedule transaction timeout */
+		if (TransactionTimeout > 0)
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -3426,6 +3430,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,7 +4506,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4504,7 +4520,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4562,6 +4579,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5140,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 8e97a0150f..8f1157afee 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 60bc1217fb..fd586c193c 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 552cf9d950..64be4de0c7 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 3945a92ddd..fcb214a04d 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2567,6 +2567,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index b2809c711a..0b37117eb7 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -700,6 +700,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4..3342971bd0 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 050a831226..39ca7e6d38 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1251,6 +1251,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 417c74cfef..9cda3f3667 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 74bc2f97cb..b2d0f84252 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index e87fd25d64..9dde9cbfdd 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -429,6 +429,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 8a61853371..608a83d5a8 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..cabe28f2c8 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 6 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,42 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4
+step stt1_set: SET transaction_timeout = '1ms';
+step stt1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_here: SELECT pg_sleep(1);
+FATAL:  terminating connection due to transaction timeout
+server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+
+step stt2_set: SET transaction_timeout = '1ms';
+step stt2_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
+count
+-----
+    0
+(1 row)
+
+step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
+step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
+step stt3_check_itt4: <... completed>
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..2772939b6b 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -18,7 +18,7 @@ step wrtbl	{ UPDATE accounts SET balance = balance + 100; }
 teardown	{ ABORT; }
 
 session s2
-setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+setup		{ SET transaction_timeout = '10s'; SET idle_in_transaction_session_timeout = '10s'; BEGIN ISOLATION LEVEL READ COMMITTED; }
 step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
@@ -27,6 +27,29 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session stt1
+# enable statement_timeout to check interaction
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt1_set	{ SET transaction_timeout = '1ms'; }
+step stt1_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step sleep_here	{ SELECT pg_sleep(1); }
+
+session stt2
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt2_set	{ SET transaction_timeout = '1ms'; }
+step stt2_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+# Session stt2 is terminated in the background. However, isolation tester needs a step to observe it.
+
+session stt3
+step sleep_there{ SELECT pg_sleep(0.1); }
+# Observe that stt2\itt4 died
+step stt3_check_stt2 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2' }
+step stt3_check_itt4 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' }
+
+session itt4
+step itt4_set	{ SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s'; }
+step itt4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +70,7 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# timeout of active query, idle transaction timeout
+permutation stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4(*)
+# can't run tests after this, sessions stt1, stt2, and itt4 are expected to FATAL-out
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v21-0004-fix-reschedule-timeout-for-each-commmand.patch (1.3K, ../../[email protected]/5-v21-0004-fix-reschedule-timeout-for-each-commmand.patch)
  download | inline diff:
From f7d67812410678c8f547c1f2038817f4426f0516 Mon Sep 17 00:00:00 2001
From: Zhao Junwang <[email protected]>
Date: Fri, 29 Dec 2023 18:41:24 +0800
Subject: [PATCH v21 4/4] fix reschedule timeout for each commmand

Signed-off-by: Zhao Junwang <[email protected]>
---
 src/backend/access/transam/xact.c | 4 ++++
 src/backend/tcop/postgres.c       | 4 ----
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 8442c5e6a7..2d9b718762 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2139,6 +2139,10 @@ StartTransaction(void)
 	 */
 	s->state = TRANS_INPROGRESS;
 
+	/* Schedule transaction timeout */
+	if (TransactionTimeout > 0)
+		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 96161eb7ab..36b9e3f8c5 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,10 +2745,6 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
-		/* Schedule or reschedule transaction timeout */
-		if (TransactionTimeout > 0)
-			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
-
 		xact_started = true;
 	}
 
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v21-0003-Try-to-enable-transaction_timeout-before-next-co.patch (1.4K, ../../[email protected]/7-v21-0003-Try-to-enable-transaction_timeout-before-next-co.patch)
  download | inline diff:
From 86b4d6f4df132da83fc4d754b54eaefe17c303ee Mon Sep 17 00:00:00 2001
From: japinli <[email protected]>
Date: Sat, 23 Dec 2023 11:04:25 +0800
Subject: [PATCH v21 3/4] Try to enable transaction_timeout before next command

---
 src/backend/tcop/postgres.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index a2611cf8e6..96161eb7ab 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -4513,6 +4513,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else if (IsTransactionOrTransactionBlock())
 			{
@@ -4527,6 +4532,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else
 			{
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v21-0002-Add-better-tests-for-transaction_timeout.patch (10.9K, ../../[email protected]/9-v21-0002-Add-better-tests-for-transaction_timeout.patch)
  download | inline diff:
From 1ed73bb8ee489483aac5522b417815d743141cc0 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Fri, 29 Dec 2023 14:54:02 +0500
Subject: [PATCH v21 2/4] Add better tests for transaction_timeout: 1. Check
 COMMIT AND CHAIN 2. Check termination of active and idle queries 3. Check
 timeout reschedult 4. Check that timeout is not rescheduled by new queries

---
 src/test/isolation/Makefile              |   3 +
 src/test/isolation/expected/timeouts.out | 123 ++++++++++++++++++++---
 src/test/isolation/specs/timeouts.spec   |  74 +++++++++-----
 3 files changed, 163 insertions(+), 37 deletions(-)

diff --git a/src/test/isolation/Makefile b/src/test/isolation/Makefile
index ade2256ed3..482bb31949 100644
--- a/src/test/isolation/Makefile
+++ b/src/test/isolation/Makefile
@@ -72,3 +72,6 @@ installcheck-prepared-txns: all temp-install
 
 check-prepared-txns: all temp-install
 	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule prepared-transactions prepared-transactions-cic
+
+check-timeouts: all temp-install
+	$(pg_isolation_regress_check) timeouts
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index cabe28f2c8..a500e9ab91 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,5 @@
-Parsed test spec with 6 sessions
+unused step name: s6_sleep
+Parsed test spec with 8 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -80,39 +81,133 @@ step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
 
-starting permutation: stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4
-step stt1_set: SET transaction_timeout = '1ms';
-step stt1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_here: SELECT pg_sleep(1);
+starting permutation: stto s3_begin s3_sleep s3_check s3_abort
+step stto: SET statement_timeout = '1ms'; SET transaction_timeout = '1s';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s3_sleep: SELECT pg_sleep(0.1);
+ERROR:  canceling statement due to statement timeout
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    1
+(1 row)
+
+step s3_abort: ABORT;
+
+starting permutation: tsto s3_begin s3_sleep s3_check
+step tsto: SET statement_timeout = '1s'; SET transaction_timeout = '1ms';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s3_sleep: SELECT pg_sleep(0.1);
 FATAL:  terminating connection due to transaction timeout
 server closed the connection unexpectedly
 	This probably means the server terminated abnormally
 	before or while processing the request.
 
-step stt2_set: SET transaction_timeout = '1ms';
-step stt2_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_there: SELECT pg_sleep(0.1);
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: itto s4_begin checker_sleep s4_check
+step itto: SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s';
+step s4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s4_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: tito s5_begin checker_sleep s5_check
+step tito: SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms';
+step s5_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s5_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: s6_begin s6_tt checker_sleep s6_check
+step s6_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s6_tt: SET statement_timeout = '1s'; SET transaction_timeout = '1ms';
+step checker_sleep: SELECT pg_sleep(0.1);
 pg_sleep
 --------
         
 (1 row)
 
-step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
+step s6_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6';
 count
 -----
     0
 (1 row)
 
-step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
-step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_there: SELECT pg_sleep(0.1);
+
+starting permutation: s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_check s7_abort
+step s7_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '150ms';
+
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_commit_and_chain: COMMIT AND CHAIN;
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7';
+count
+-----
+    1
+(1 row)
+
+step s7_abort: ABORT;
+
+starting permutation: s7_begin s7_sleep s7_select_1 checker_sleep s7_check
+step s7_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '150ms';
+
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_select_1: SELECT 1;
+?column?
+--------
+       1
+(1 row)
+
+step checker_sleep: SELECT pg_sleep(0.1);
 pg_sleep
 --------
         
 (1 row)
 
-step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
-step stt3_check_itt4: <... completed>
+step s7_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7';
 count
 -----
     0
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index 2772939b6b..dc47d4f362 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -18,7 +18,7 @@ step wrtbl	{ UPDATE accounts SET balance = balance + 100; }
 teardown	{ ABORT; }
 
 session s2
-setup		{ SET transaction_timeout = '10s'; SET idle_in_transaction_session_timeout = '10s'; BEGIN ISOLATION LEVEL READ COMMITTED; }
+setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
 step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
@@ -27,28 +27,45 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
-session stt1
-# enable statement_timeout to check interaction
-setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
-step stt1_set	{ SET transaction_timeout = '1ms'; }
-step stt1_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
-step sleep_here	{ SELECT pg_sleep(1); }
+session s3
+step s3_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step stto	{ SET statement_timeout = '1ms'; SET transaction_timeout = '1s'; }
+step tsto	{ SET statement_timeout = '1s'; SET transaction_timeout = '1ms'; }
+step s3_sleep	{ SELECT pg_sleep(0.1); }
+step s3_abort	{ ABORT; }
+
+session s4
+step s4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step itto	{ SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s'; }
+
+session s5
+step s5_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step tito	{ SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms'; }
 
-session stt2
-setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
-step stt2_set	{ SET transaction_timeout = '1ms'; }
-step stt2_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
-# Session stt2 is terminated in the background. However, isolation tester needs a step to observe it.
+session s6
+step s6_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step s6_tt	{ SET statement_timeout = '1s'; SET transaction_timeout = '1ms'; }
+step s6_sleep	{ SELECT pg_sleep(0.1); }
 
-session stt3
-step sleep_there{ SELECT pg_sleep(0.1); }
-# Observe that stt2\itt4 died
-step stt3_check_stt2 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2' }
-step stt3_check_itt4 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' }
+session s7
+step s7_begin
+{
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '150ms';
+}
+step s7_commit_and_chain { COMMIT AND CHAIN; }
+# to test that quick query does not restart transaction_timeout
+step s7_select_1 { SELECT 1; }
+step s7_sleep	{ SELECT pg_sleep(0.1); }
+step s7_abort	{ ABORT; }
 
-session itt4
-step itt4_set	{ SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s'; }
-step itt4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+session checker
+step checker_sleep	{ SELECT pg_sleep(0.1); }
+step s3_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3'; }
+step s4_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4'; }
+step s5_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5'; }
+step s6_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6'; }
+step s7_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7'; }
 
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
@@ -71,6 +88,17 @@ permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
 
-# timeout of active query, idle transaction timeout
-permutation stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4(*)
-# can't run tests after this, sessions stt1, stt2, and itt4 are expected to FATAL-out
+# statement timeout expires first
+permutation stto s3_begin s3_sleep s3_check s3_abort
+# transaction timeout expires first, session s3 FATAL-out
+permutation tsto s3_begin s3_sleep s3_check
+# idle in transaction timeout expires first, session s4 FATAL-out
+permutation itto s4_begin checker_sleep s4_check
+# transaction timeout expires first, session s5 FATAL-out
+permutation tito s5_begin checker_sleep s5_check
+# transaction timeout can be schedule amid transaction, session s6 FATAL-out
+permutation s6_begin s6_tt checker_sleep s6_check
+# COMMIT AND CHAIN must restart transaction timeout
+permutation s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_check s7_abort
+# transaction timeout expires in presence of query flow, session s7 FATAL-out
+permutation s7_begin s7_sleep s7_select_1 checker_sleep s7_check
-- 
2.37.1 (Apple Git-137.1)



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-01-03 12:04  Andrey M. Borodin <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2024-01-03 12:04 UTC (permalink / raw)
  To: Junwang Zhao <[email protected]>; +Cc: Li Japin <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>



> On 3 Jan 2024, at 16:46, Andrey M. Borodin <[email protected]> wrote:
> 
>  I do not understand why, but mailing list did not pick patches that I sent. I'll retry.


Sorry for the noise. Seems like Apple updated something in Mail.App couple of days ago and it started to use strange "Apple-Mail" stuff by default.
I see patches were attached, but were not recognized by mailing list archives and CFbot.
Now I've flipped everything to "plain text by default" everywhere. Hope that helps.


Best regards, Andrey Borodin.


Attachments:

  [application/octet-stream] v21-0001-Introduce-transaction_timeout.patch (19.8K, ../../[email protected]/2-v21-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From d9f6e4d7c7183fe6042f11d98270f707f87b9e97 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v21 1/4] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>
Reviewed-by: Japin Li <[email protected]>
Reviewed-by: Junwang Zhao <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 35 ++++++++++++++++
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 27 ++++++++++--
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 +++++
 src/backend/utils/misc/guc_tables.c           | 11 +++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/expected/timeouts.out      | 41 ++++++++++++++++++-
 src/test/isolation/specs/timeouts.spec        | 29 ++++++++++++-
 17 files changed, 162 insertions(+), 5 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f323bba018..0d849a11ce 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9134,6 +9134,41 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index b04fcfc8c8..e6fa1cfdc2 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b6451d9d08..4be06c1e5d 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7298a187d1..a2611cf8e6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,6 +2745,10 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		/* Schedule or reschedule transaction timeout */
+		if (TransactionTimeout > 0)
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -3426,6 +3430,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,7 +4506,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4504,7 +4520,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4562,6 +4579,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5140,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 8e97a0150f..8f1157afee 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 60bc1217fb..fd586c193c 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 552cf9d950..64be4de0c7 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 3945a92ddd..fcb214a04d 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2567,6 +2567,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index b2809c711a..0b37117eb7 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -700,6 +700,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4..3342971bd0 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 050a831226..39ca7e6d38 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1251,6 +1251,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 417c74cfef..9cda3f3667 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 74bc2f97cb..b2d0f84252 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index e87fd25d64..9dde9cbfdd 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -429,6 +429,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 8a61853371..608a83d5a8 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..cabe28f2c8 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 6 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,42 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4
+step stt1_set: SET transaction_timeout = '1ms';
+step stt1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_here: SELECT pg_sleep(1);
+FATAL:  terminating connection due to transaction timeout
+server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+
+step stt2_set: SET transaction_timeout = '1ms';
+step stt2_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
+count
+-----
+    0
+(1 row)
+
+step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
+step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step sleep_there: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
+step stt3_check_itt4: <... completed>
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..2772939b6b 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -18,7 +18,7 @@ step wrtbl	{ UPDATE accounts SET balance = balance + 100; }
 teardown	{ ABORT; }
 
 session s2
-setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+setup		{ SET transaction_timeout = '10s'; SET idle_in_transaction_session_timeout = '10s'; BEGIN ISOLATION LEVEL READ COMMITTED; }
 step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
@@ -27,6 +27,29 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session stt1
+# enable statement_timeout to check interaction
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt1_set	{ SET transaction_timeout = '1ms'; }
+step stt1_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step sleep_here	{ SELECT pg_sleep(1); }
+
+session stt2
+setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
+step stt2_set	{ SET transaction_timeout = '1ms'; }
+step stt2_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+# Session stt2 is terminated in the background. However, isolation tester needs a step to observe it.
+
+session stt3
+step sleep_there{ SELECT pg_sleep(0.1); }
+# Observe that stt2\itt4 died
+step stt3_check_stt2 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2' }
+step stt3_check_itt4 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' }
+
+session itt4
+step itt4_set	{ SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s'; }
+step itt4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +70,7 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# timeout of active query, idle transaction timeout
+permutation stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4(*)
+# can't run tests after this, sessions stt1, stt2, and itt4 are expected to FATAL-out
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v21-0004-fix-reschedule-timeout-for-each-commmand.patch (1.3K, ../../[email protected]/3-v21-0004-fix-reschedule-timeout-for-each-commmand.patch)
  download | inline diff:
From f7d67812410678c8f547c1f2038817f4426f0516 Mon Sep 17 00:00:00 2001
From: Zhao Junwang <[email protected]>
Date: Fri, 29 Dec 2023 18:41:24 +0800
Subject: [PATCH v21 4/4] fix reschedule timeout for each commmand

Signed-off-by: Zhao Junwang <[email protected]>
---
 src/backend/access/transam/xact.c | 4 ++++
 src/backend/tcop/postgres.c       | 4 ----
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 8442c5e6a7..2d9b718762 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2139,6 +2139,10 @@ StartTransaction(void)
 	 */
 	s->state = TRANS_INPROGRESS;
 
+	/* Schedule transaction timeout */
+	if (TransactionTimeout > 0)
+		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 96161eb7ab..36b9e3f8c5 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,10 +2745,6 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
-		/* Schedule or reschedule transaction timeout */
-		if (TransactionTimeout > 0)
-			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
-
 		xact_started = true;
 	}
 
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v21-0003-Try-to-enable-transaction_timeout-before-next-co.patch (1.4K, ../../[email protected]/4-v21-0003-Try-to-enable-transaction_timeout-before-next-co.patch)
  download | inline diff:
From 86b4d6f4df132da83fc4d754b54eaefe17c303ee Mon Sep 17 00:00:00 2001
From: japinli <[email protected]>
Date: Sat, 23 Dec 2023 11:04:25 +0800
Subject: [PATCH v21 3/4] Try to enable transaction_timeout before next command

---
 src/backend/tcop/postgres.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index a2611cf8e6..96161eb7ab 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -4513,6 +4513,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else if (IsTransactionOrTransactionBlock())
 			{
@@ -4527,6 +4532,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else
 			{
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v21-0002-Add-better-tests-for-transaction_timeout.patch (10.9K, ../../[email protected]/5-v21-0002-Add-better-tests-for-transaction_timeout.patch)
  download | inline diff:
From 1ed73bb8ee489483aac5522b417815d743141cc0 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Fri, 29 Dec 2023 14:54:02 +0500
Subject: [PATCH v21 2/4] Add better tests for transaction_timeout: 1. Check
 COMMIT AND CHAIN 2. Check termination of active and idle queries 3. Check
 timeout reschedult 4. Check that timeout is not rescheduled by new queries

---
 src/test/isolation/Makefile              |   3 +
 src/test/isolation/expected/timeouts.out | 123 ++++++++++++++++++++---
 src/test/isolation/specs/timeouts.spec   |  74 +++++++++-----
 3 files changed, 163 insertions(+), 37 deletions(-)

diff --git a/src/test/isolation/Makefile b/src/test/isolation/Makefile
index ade2256ed3..482bb31949 100644
--- a/src/test/isolation/Makefile
+++ b/src/test/isolation/Makefile
@@ -72,3 +72,6 @@ installcheck-prepared-txns: all temp-install
 
 check-prepared-txns: all temp-install
 	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule prepared-transactions prepared-transactions-cic
+
+check-timeouts: all temp-install
+	$(pg_isolation_regress_check) timeouts
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index cabe28f2c8..a500e9ab91 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,5 @@
-Parsed test spec with 6 sessions
+unused step name: s6_sleep
+Parsed test spec with 8 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -80,39 +81,133 @@ step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
 
-starting permutation: stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4
-step stt1_set: SET transaction_timeout = '1ms';
-step stt1_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_here: SELECT pg_sleep(1);
+starting permutation: stto s3_begin s3_sleep s3_check s3_abort
+step stto: SET statement_timeout = '1ms'; SET transaction_timeout = '1s';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s3_sleep: SELECT pg_sleep(0.1);
+ERROR:  canceling statement due to statement timeout
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    1
+(1 row)
+
+step s3_abort: ABORT;
+
+starting permutation: tsto s3_begin s3_sleep s3_check
+step tsto: SET statement_timeout = '1s'; SET transaction_timeout = '1ms';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s3_sleep: SELECT pg_sleep(0.1);
 FATAL:  terminating connection due to transaction timeout
 server closed the connection unexpectedly
 	This probably means the server terminated abnormally
 	before or while processing the request.
 
-step stt2_set: SET transaction_timeout = '1ms';
-step stt2_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_there: SELECT pg_sleep(0.1);
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: itto s4_begin checker_sleep s4_check
+step itto: SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s';
+step s4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s4_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: tito s5_begin checker_sleep s5_check
+step tito: SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms';
+step s5_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s5_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: s6_begin s6_tt checker_sleep s6_check
+step s6_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s6_tt: SET statement_timeout = '1s'; SET transaction_timeout = '1ms';
+step checker_sleep: SELECT pg_sleep(0.1);
 pg_sleep
 --------
         
 (1 row)
 
-step stt3_check_stt2: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2'
+step s6_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6';
 count
 -----
     0
 (1 row)
 
-step itt4_set: SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s';
-step itt4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step sleep_there: SELECT pg_sleep(0.1);
+
+starting permutation: s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_check s7_abort
+step s7_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '150ms';
+
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_commit_and_chain: COMMIT AND CHAIN;
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7';
+count
+-----
+    1
+(1 row)
+
+step s7_abort: ABORT;
+
+starting permutation: s7_begin s7_sleep s7_select_1 checker_sleep s7_check
+step s7_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '150ms';
+
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_select_1: SELECT 1;
+?column?
+--------
+       1
+(1 row)
+
+step checker_sleep: SELECT pg_sleep(0.1);
 pg_sleep
 --------
         
 (1 row)
 
-step stt3_check_itt4: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' <waiting ...>
-step stt3_check_itt4: <... completed>
+step s7_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7';
 count
 -----
     0
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index 2772939b6b..dc47d4f362 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -18,7 +18,7 @@ step wrtbl	{ UPDATE accounts SET balance = balance + 100; }
 teardown	{ ABORT; }
 
 session s2
-setup		{ SET transaction_timeout = '10s'; SET idle_in_transaction_session_timeout = '10s'; BEGIN ISOLATION LEVEL READ COMMITTED; }
+setup		{ BEGIN ISOLATION LEVEL READ COMMITTED; }
 step sto	{ SET statement_timeout = '10ms'; }
 step lto	{ SET lock_timeout = '10ms'; }
 step lsto	{ SET lock_timeout = '10ms'; SET statement_timeout = '10s'; }
@@ -27,28 +27,45 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
-session stt1
-# enable statement_timeout to check interaction
-setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
-step stt1_set	{ SET transaction_timeout = '1ms'; }
-step stt1_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
-step sleep_here	{ SELECT pg_sleep(1); }
+session s3
+step s3_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step stto	{ SET statement_timeout = '1ms'; SET transaction_timeout = '1s'; }
+step tsto	{ SET statement_timeout = '1s'; SET transaction_timeout = '1ms'; }
+step s3_sleep	{ SELECT pg_sleep(0.1); }
+step s3_abort	{ ABORT; }
+
+session s4
+step s4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step itto	{ SET idle_in_transaction_session_timeout = '1ms'; SET transaction_timeout = '1s'; }
+
+session s5
+step s5_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step tito	{ SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '1ms'; }
 
-session stt2
-setup			{ SET statement_timeout = '10s'; SET lock_timeout = '10s'; }
-step stt2_set	{ SET transaction_timeout = '1ms'; }
-step stt2_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
-# Session stt2 is terminated in the background. However, isolation tester needs a step to observe it.
+session s6
+step s6_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step s6_tt	{ SET statement_timeout = '1s'; SET transaction_timeout = '1ms'; }
+step s6_sleep	{ SELECT pg_sleep(0.1); }
 
-session stt3
-step sleep_there{ SELECT pg_sleep(0.1); }
-# Observe that stt2\itt4 died
-step stt3_check_stt2 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/stt2' }
-step stt3_check_itt4 { SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/itt4' }
+session s7
+step s7_begin
+{
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '150ms';
+}
+step s7_commit_and_chain { COMMIT AND CHAIN; }
+# to test that quick query does not restart transaction_timeout
+step s7_select_1 { SELECT 1; }
+step s7_sleep	{ SELECT pg_sleep(0.1); }
+step s7_abort	{ ABORT; }
 
-session itt4
-step itt4_set	{ SET idle_in_transaction_session_timeout = '1ms'; SET statement_timeout = '10s'; SET lock_timeout = '10s'; SET transaction_timeout = '10s'; }
-step itt4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+session checker
+step checker_sleep	{ SELECT pg_sleep(0.1); }
+step s3_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3'; }
+step s4_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4'; }
+step s5_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5'; }
+step s6_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6'; }
+step s7_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7'; }
 
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
@@ -71,6 +88,17 @@ permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
 
-# timeout of active query, idle transaction timeout
-permutation stt1_set stt1_begin sleep_here stt2_set stt2_begin sleep_there stt3_check_stt2 itt4_set itt4_begin sleep_there stt3_check_itt4(*)
-# can't run tests after this, sessions stt1, stt2, and itt4 are expected to FATAL-out
+# statement timeout expires first
+permutation stto s3_begin s3_sleep s3_check s3_abort
+# transaction timeout expires first, session s3 FATAL-out
+permutation tsto s3_begin s3_sleep s3_check
+# idle in transaction timeout expires first, session s4 FATAL-out
+permutation itto s4_begin checker_sleep s4_check
+# transaction timeout expires first, session s5 FATAL-out
+permutation tito s5_begin checker_sleep s5_check
+# transaction timeout can be schedule amid transaction, session s6 FATAL-out
+permutation s6_begin s6_tt checker_sleep s6_check
+# COMMIT AND CHAIN must restart transaction timeout
+permutation s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_check s7_abort
+# transaction timeout expires in presence of query flow, session s7 FATAL-out
+permutation s7_begin s7_sleep s7_select_1 checker_sleep s7_check
-- 
2.37.1 (Apple Git-137.1)



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-01-04 02:14  Japin Li <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Japin Li @ 2024-01-04 02:14 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>


On Wed, 03 Jan 2024 at 20:04, Andrey M. Borodin <[email protected]> wrote:
>> On 3 Jan 2024, at 16:46, Andrey M. Borodin <[email protected]> wrote:
>>
>>  I do not understand why, but mailing list did not pick patches that I sent. I'll retry.
>
>
> Sorry for the noise. Seems like Apple updated something in Mail.App couple of days ago and it started to use strange "Apple-Mail" stuff by default.
> I see patches were attached, but were not recognized by mailing list archives and CFbot.
> Now I've flipped everything to "plain text by default" everywhere. Hope that helps.
>

Thanks for updating the patch, I find the test on Debian with mason failed [1].

Does the timeout is too short for testing? I see the timeouts for lock_timeout
and statement_timeout is more bigger than transaction_timeout.

[1] https://api.cirrus-ci.com/v1/artifact/task/5490718928535552/testrun/build-32/testrun/isolation/isola...






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-01-04 08:41  Andrey M. Borodin <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2024-01-04 08:41 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>



> On 4 Jan 2024, at 07:14, Japin Li <[email protected]> wrote:
> 
> Does the timeout is too short for testing? I see the timeouts for lock_timeout
> and statement_timeout is more bigger than transaction_timeout.

Makes sense. Done. I've also put some effort into fine-tuning timeouts Nik's case tests. To have 100ms gap between check, false positive and actual bug we had I had to use transaction_timeout = 300ms. Currently all tests take more than 1000ms!
But I do not see a way to make these tests both stable and short.


Best regards, Andrey Borodin.


Attachments:

  [application/octet-stream] v22-0001-Introduce-transaction_timeout.patch (15.2K, ../../[email protected]/2-v22-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From 97fd0d9b63b71fbb2ef24362774a80c605a43abd Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v22 1/4] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>
Reviewed-by: Japin Li <[email protected]>
Reviewed-by: Junwang Zhao <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 35 +++++++++++++++++++
 src/backend/postmaster/autovacuum.c           |  2 ++
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 27 ++++++++++++--
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 ++++++
 src/backend/utils/misc/guc_tables.c           | 11 ++++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 ++
 src/bin/pg_dump/pg_dump.c                     |  2 ++
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 15 files changed, 94 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f323bba018..0d849a11ce 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9134,6 +9134,41 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index b04fcfc8c8..e6fa1cfdc2 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index b6451d9d08..4be06c1e5d 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7298a187d1..a2611cf8e6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,6 +2745,10 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
+		/* Schedule or reschedule transaction timeout */
+		if (TransactionTimeout > 0)
+			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 		xact_started = true;
 	}
 
@@ -3426,6 +3430,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,7 +4506,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4504,7 +4520,8 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
@@ -4562,6 +4579,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5140,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 8e97a0150f..8f1157afee 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 60bc1217fb..fd586c193c 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 552cf9d950..64be4de0c7 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 3945a92ddd..fcb214a04d 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2567,6 +2567,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index b2809c711a..0b37117eb7 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -700,6 +700,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4..3342971bd0 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 050a831226..39ca7e6d38 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1251,6 +1251,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 417c74cfef..9cda3f3667 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 74bc2f97cb..b2d0f84252 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index e87fd25d64..9dde9cbfdd 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -429,6 +429,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 8a61853371..608a83d5a8 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v22-0002-Add-tests-for-transaction_timeout.patch (9.2K, ../../[email protected]/3-v22-0002-Add-tests-for-transaction_timeout.patch)
  download | inline diff:
From 01dd5b80ed0ebbdf5546d69db4ac497d8c5f397f Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Fri, 29 Dec 2023 14:54:02 +0500
Subject: [PATCH v22 2/4] Add tests for transaction_timeout: 0. Check
 interaction with other timeouts 1. Check COMMIT AND CHAIN 2. Check
 termination of active and idle queries 3. Check timeout rescheduled 4. Check
 that timeout is not rescheduled by new queries

---
 src/test/isolation/Makefile              |   3 +
 src/test/isolation/expected/timeouts.out | 153 ++++++++++++++++++++++-
 src/test/isolation/specs/timeouts.spec   |  67 ++++++++++
 3 files changed, 222 insertions(+), 1 deletion(-)

diff --git a/src/test/isolation/Makefile b/src/test/isolation/Makefile
index ade2256ed3..482bb31949 100644
--- a/src/test/isolation/Makefile
+++ b/src/test/isolation/Makefile
@@ -72,3 +72,6 @@ installcheck-prepared-txns: all temp-install
 
 check-prepared-txns: all temp-install
 	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule prepared-transactions prepared-transactions-cic
+
+check-timeouts: all temp-install
+	$(pg_isolation_regress_check) timeouts
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1c..fcfd981095 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 9 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,154 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stto s3_begin s3_sleep s3_check s3_abort
+step stto: SET statement_timeout = '10ms'; SET transaction_timeout = '1s';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s3_sleep: SELECT pg_sleep(0.1);
+ERROR:  canceling statement due to statement timeout
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    1
+(1 row)
+
+step s3_abort: ABORT;
+
+starting permutation: tsto s3_begin s3_sleep s3_check
+step tsto: SET statement_timeout = '1s'; SET transaction_timeout = '10ms';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s3_sleep: SELECT pg_sleep(0.1);
+FATAL:  terminating connection due to transaction timeout
+server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: itto s4_begin checker_sleep s4_check
+step itto: SET idle_in_transaction_session_timeout = '10ms'; SET transaction_timeout = '1s';
+step s4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s4_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: tito s5_begin checker_sleep s5_check
+step tito: SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '10ms';
+step s5_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s5_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: s6_begin s6_tt checker_sleep s6_check
+step s6_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s6_tt: SET statement_timeout = '1s'; SET transaction_timeout = '10ms';
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s6_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: s7_begin s7_sleep s7_sleep s7_commit_and_chain s7_sleep s7_check s7_abort
+step s7_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '300ms';
+
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_commit_and_chain: COMMIT AND CHAIN;
+step s7_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7';
+count
+-----
+    1
+(1 row)
+
+step s7_abort: ABORT;
+
+starting permutation: s8_begin s8_sleep s8_sleep s8_select_1 checker_sleep checker_sleep s8_check
+step s8_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '300ms';
+
+step s8_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s8_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s8_select_1: SELECT 1;
+?column?
+--------
+       1
+(1 row)
+
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s8_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s8';
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28..29c601ed4a 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -27,6 +27,54 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session s3
+step s3_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step stto	{ SET statement_timeout = '10ms'; SET transaction_timeout = '1s'; }
+step tsto	{ SET statement_timeout = '1s'; SET transaction_timeout = '10ms'; }
+step s3_sleep	{ SELECT pg_sleep(0.1); }
+step s3_abort	{ ABORT; }
+
+session s4
+step s4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step itto	{ SET idle_in_transaction_session_timeout = '10ms'; SET transaction_timeout = '1s'; }
+
+session s5
+step s5_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step tito	{ SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '10ms'; }
+
+session s6
+step s6_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step s6_tt	{ SET statement_timeout = '1s'; SET transaction_timeout = '10ms'; }
+
+session s7
+step s7_begin
+{
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '300ms';
+}
+step s7_commit_and_chain { COMMIT AND CHAIN; }
+step s7_sleep	{ SELECT pg_sleep(0.1); }
+step s7_abort	{ ABORT; }
+
+session s8
+step s8_begin
+{
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '300ms';
+}
+# to test that quick query does not restart transaction_timeout
+step s8_select_1 { SELECT 1; }
+step s8_sleep	{ SELECT pg_sleep(0.1); }
+
+session checker
+step checker_sleep	{ SELECT pg_sleep(0.1); }
+step s3_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3'; }
+step s4_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4'; }
+step s5_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5'; }
+step s6_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6'; }
+step s7_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7'; }
+step s8_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s8'; }
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +95,22 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# statement timeout expires first
+permutation stto s3_begin s3_sleep s3_check s3_abort
+# transaction timeout expires first, session s3 FATAL-out
+permutation tsto s3_begin s3_sleep s3_check
+# idle in transaction timeout expires first, session s4 FATAL-out
+permutation itto s4_begin checker_sleep s4_check
+# transaction timeout expires first, session s5 FATAL-out
+permutation tito s5_begin checker_sleep s5_check
+# transaction timeout can be schedule amid transaction, session s6 FATAL-out
+permutation s6_begin s6_tt checker_sleep s6_check
+# COMMIT AND CHAIN must restart transaction timeout
+permutation s7_begin s7_sleep s7_sleep s7_commit_and_chain s7_sleep s7_check s7_abort
+# transaction timeout expires in presence of query flow, session s7 FATAL-out
+# this relatevely long sleeps are picked to ensure 100ms gap between check and timeouts firing
+# expected flow: timeouts is scheduled after s8_begin and fires approximately after checker_sleep (100ms before check)
+# possible buggy flow: timeout is schedules after s8_select_1 and fires 100ms after s8_check
+# to ensure this 100ms gap we need minimum transaction_timeout of 300ms
+permutation s8_begin s8_sleep s8_sleep s8_select_1 checker_sleep checker_sleep s8_check
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v22-0003-Try-to-enable-transaction_timeout-before-next-co.patch (1.4K, ../../[email protected]/4-v22-0003-Try-to-enable-transaction_timeout-before-next-co.patch)
  download | inline diff:
From eab988d4161f188b9ecdf3e205f738130c7785aa Mon Sep 17 00:00:00 2001
From: japinli <[email protected]>
Date: Sat, 23 Dec 2023 11:04:25 +0800
Subject: [PATCH v22 3/4] Try to enable transaction_timeout before next command

---
 src/backend/tcop/postgres.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index a2611cf8e6..96161eb7ab 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -4513,6 +4513,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else if (IsTransactionOrTransactionBlock())
 			{
@@ -4527,6 +4532,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else
 			{
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] v22-0004-fix-reschedule-timeout-for-each-commmand.patch (1.3K, ../../[email protected]/5-v22-0004-fix-reschedule-timeout-for-each-commmand.patch)
  download | inline diff:
From 82a2fbe8f60e6e1bd82cdb07777962db425e61a3 Mon Sep 17 00:00:00 2001
From: Zhao Junwang <[email protected]>
Date: Fri, 29 Dec 2023 18:41:24 +0800
Subject: [PATCH v22 4/4] fix reschedule timeout for each commmand

Signed-off-by: Zhao Junwang <[email protected]>
---
 src/backend/access/transam/xact.c | 4 ++++
 src/backend/tcop/postgres.c       | 4 ----
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 8442c5e6a7..2d9b718762 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2139,6 +2139,10 @@ StartTransaction(void)
 	 */
 	s->state = TRANS_INPROGRESS;
 
+	/* Schedule transaction timeout */
+	if (TransactionTimeout > 0)
+		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 96161eb7ab..36b9e3f8c5 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2745,10 +2745,6 @@ start_xact_command(void)
 	{
 		StartTransactionCommand();
 
-		/* Schedule or reschedule transaction timeout */
-		if (TransactionTimeout > 0)
-			enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
-
 		xact_started = true;
 	}
 
-- 
2.37.1 (Apple Git-137.1)



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-01-22 06:23  Peter Smith <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Peter Smith @ 2024-01-22 06:23 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: Japin Li <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>

2024-01 Commitfest.

Hi, This patch has a CF status of "Needs Review" [1], but it seems
there was a CFbot test failure last time it was run [2]. Please have a
look and post an updated version if necessary.

======
[1] https://commitfest.postgresql.org/46/4040/
[2] https://cirrus-ci.com/task/4721191139672064

Kind Regards,
Peter Smith.





^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-01-26 06:44  Andrey M. Borodin <[email protected]>
  parent: Peter Smith <[email protected]>
  0 siblings, 2 replies; 76+ messages in thread

From: Andrey M. Borodin @ 2024-01-26 06:44 UTC (permalink / raw)
  To: Peter Smith <[email protected]>; +Cc: Japin Li <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>



> On 22 Jan 2024, at 11:23, Peter Smith <[email protected]> wrote:
> 
> Hi, This patch has a CF status of "Needs Review" [1], but it seems
> there was a CFbot test failure last time it was run [2]. Please have a
> look and post an updated version if necessary.
Thanks Peter!

I’ve inspected CI fails and they were caused by two different problems:
1. It’s unsafe for isaoltion tester to await transaction_timeout within a query. Usually it gets
FATAL: terminating connection due to transaction timeout
But if VM is a bit slow it can get occasional
PQconsumeInput failed: server closed the connection unexpectedly
So, currently all tests use “passive waiting”, in a session that will not timeout.

2. In some cases pg_sleep(0.1) were sleeping up to 200 ms. That was making s7 and s8 fail, because they rely on this margin.
I’ve separated these tests into different test timeouts-long and increased margin to 300ms. Now tests run horrible 2431 ms. Moreover I’m afraid that on buildfarm we can have much randomly-slower machines so this test might be excluded.
This test checks COMMIT AND CHAIN and flow of small queries (Nik’s case).

Also I’ve verified that every "enable_timeout_after(TRANSACTION_TIMEOUT)” and “disable_timeout(TRANSACTION_TIMEOUT)” is necessary and found that case of aborting "idle in transaction (aborted)” is not covered by tests. I’m not sure we need a test for this.
Japin, Junwang, what do you think?

Thanks!


Best regards, Andrey Borodin.



Attachments:

  [application/octet-stream] v23-0001-Introduce-transaction_timeout.patch (26.3K, ../../[email protected]/2-v23-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From 7b0a01b9fd47130c033e74188601ff7d78781084 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v23] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Author: Japin Li <[email protected]>
Author: Junwang Zhao <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 35 ++++++++
 src/backend/access/transam/xact.c             |  4 +
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 33 +++++++-
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 +++
 src/backend/utils/misc/guc_tables.c           | 11 +++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/Makefile                   |  3 +
 src/test/isolation/expected/timeouts-long.out | 69 ++++++++++++++++
 src/test/isolation/expected/timeouts.out      | 79 ++++++++++++++++++-
 src/test/isolation/isolation_schedule         |  1 +
 src/test/isolation/specs/timeouts-long.spec   | 35 ++++++++
 src/test/isolation/specs/timeouts.spec        | 40 +++++++++-
 22 files changed, 329 insertions(+), 5 deletions(-)
 create mode 100644 src/test/isolation/expected/timeouts-long.out
 create mode 100644 src/test/isolation/specs/timeouts-long.spec

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 61038472c5a..bd099d06350 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9135,6 +9135,41 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 464858117e0..a124ba59330 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2139,6 +2139,10 @@ StartTransaction(void)
 	 */
 	s->state = TRANS_INPROGRESS;
 
+	/* Schedule transaction timeout */
+	if (TransactionTimeout > 0)
+		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 2c3099f76f1..c12fc6594ce 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 4ad96beb87a..b8234ef8e4b 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1a34bd3715f..af28f425ce6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3426,6 +3426,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,12 +4502,18 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else if (IsTransactionOrTransactionBlock())
 			{
@@ -4504,12 +4521,18 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else
 			{
@@ -4562,6 +4585,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5146,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 29f367a5e1c..3250d539e1c 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 88b03e8fa3c..f024b1a8497 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 1ad33671598..7797876d008 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 7fe58518d7d..0fb5ec648e4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2577,6 +2577,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index da10b43dac3..3b8992f0fbf 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -701,6 +701,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4e..3342971bd01 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bc20a025ce4..c2076f979a3 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1252,6 +1252,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 11347ab1824..7d898c3b501 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0b01c1f0935..0445fbf61d7 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 4bc226e36cd..20d6fa652dc 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -429,6 +429,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 20e7cf72d0d..a5d8f078246 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/Makefile b/src/test/isolation/Makefile
index ade2256ed3a..91307e1a7e8 100644
--- a/src/test/isolation/Makefile
+++ b/src/test/isolation/Makefile
@@ -72,3 +72,6 @@ installcheck-prepared-txns: all temp-install
 
 check-prepared-txns: all temp-install
 	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule prepared-transactions prepared-transactions-cic
+
+check-timeouts: all temp-install
+	$(pg_isolation_regress_check) timeouts timeouts-long
diff --git a/src/test/isolation/expected/timeouts-long.out b/src/test/isolation/expected/timeouts-long.out
new file mode 100644
index 00000000000..26a6672c051
--- /dev/null
+++ b/src/test/isolation/expected/timeouts-long.out
@@ -0,0 +1,69 @@
+Parsed test spec with 3 sessions
+
+starting permutation: s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_check s7_abort
+step s7_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '1s';
+
+step s7_sleep: SELECT pg_sleep(0.6);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_commit_and_chain: COMMIT AND CHAIN;
+step s7_sleep: SELECT pg_sleep(0.6);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7';
+count
+-----
+    0
+(1 row)
+
+step s7_abort: ABORT;
+
+starting permutation: s8_begin s8_sleep s8_select_1 s8_check checker_sleep checker_sleep s8_check
+step s8_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '900ms';
+
+step s8_sleep: SELECT pg_sleep(0.6);
+pg_sleep
+--------
+        
+(1 row)
+
+step s8_select_1: SELECT 1;
+?column?
+--------
+       1
+(1 row)
+
+step s8_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s8';
+count
+-----
+    0
+(1 row)
+
+step checker_sleep: SELECT pg_sleep(0.3);
+pg_sleep
+--------
+        
+(1 row)
+
+step checker_sleep: SELECT pg_sleep(0.3);
+pg_sleep
+--------
+        
+(1 row)
+
+step s8_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s8';
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1cc..197c25a9a94 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 7 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,80 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stto s3_begin s3_sleep s3_check s3_abort
+step stto: SET statement_timeout = '10ms'; SET transaction_timeout = '1s';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s3_sleep: SELECT pg_sleep(0.1);
+ERROR:  canceling statement due to statement timeout
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    1
+(1 row)
+
+step s3_abort: ABORT;
+
+starting permutation: tsto s3_begin s3_sleep s3_check
+step tsto: SET statement_timeout = '1s'; SET transaction_timeout = '10ms';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s3_sleep: SELECT pg_sleep(0.1);
+FATAL:  terminating connection due to transaction timeout
+server closed the connection unexpectedly
+	This probably means the server terminated abnormally
+	before or while processing the request.
+
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: itto s4_begin checker_sleep s4_check
+step itto: SET idle_in_transaction_session_timeout = '10ms'; SET transaction_timeout = '1s';
+step s4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s4_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: tito s5_begin checker_sleep s5_check
+step tito: SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '10ms';
+step s5_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s5_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: s6_begin s6_tt checker_sleep s6_check
+step s6_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s6_tt: SET statement_timeout = '1s'; SET transaction_timeout = '10ms';
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s6_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6';
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b2be88ead1d..86ef62bbcf6 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -89,6 +89,7 @@ test: sequence-ddl
 test: async-notify
 test: vacuum-no-cleanup-lock
 test: timeouts
+test: timeouts-long
 test: vacuum-concurrent-drop
 test: vacuum-conflict
 test: vacuum-skip-locked
diff --git a/src/test/isolation/specs/timeouts-long.spec b/src/test/isolation/specs/timeouts-long.spec
new file mode 100644
index 00000000000..ce2c9a43011
--- /dev/null
+++ b/src/test/isolation/specs/timeouts-long.spec
@@ -0,0 +1,35 @@
+# Tests for transaction timeout that require long wait times
+
+session s7
+step s7_begin
+{
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '1s';
+}
+step s7_commit_and_chain { COMMIT AND CHAIN; }
+step s7_sleep	{ SELECT pg_sleep(0.6); }
+step s7_abort	{ ABORT; }
+
+session s8
+step s8_begin
+{
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '900ms';
+}
+# to test that quick query does not restart transaction_timeout
+step s8_select_1 { SELECT 1; }
+step s8_sleep	{ SELECT pg_sleep(0.6); }
+
+session checker
+step checker_sleep	{ SELECT pg_sleep(0.3); }
+step s7_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7'; }
+step s8_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s8'; }
+
+# COMMIT AND CHAIN must restart transaction timeout
+permutation s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_check s7_abort
+# transaction timeout expires in presence of query flow, session s7 FATAL-out
+# this relatevely long sleeps are picked to ensure 300ms gap between check and timeouts firing
+# expected flow: timeouts is scheduled after s8_begin and fires approximately after checker_sleep (300ms before check)
+# possible buggy flow: timeout is schedules after s8_select_1 and fires 300ms after s8_check
+# to ensure this 300ms gap we need minimum transaction_timeout of 300ms
+permutation s8_begin s8_sleep s8_select_1 s8_check checker_sleep checker_sleep s8_check
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28d..8560f01a6b3 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -1,4 +1,4 @@
-# Simple tests for statement_timeout and lock_timeout features
+# Simple tests for statement_timeout, lock_timeout and transaction_timeout features
 
 setup
 {
@@ -27,6 +27,33 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session s3
+step s3_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step stto	{ SET statement_timeout = '10ms'; SET transaction_timeout = '1s'; }
+step tsto	{ SET statement_timeout = '1s'; SET transaction_timeout = '10ms'; }
+step s3_sleep	{ SELECT pg_sleep(0.1); }
+step s3_abort	{ ABORT; }
+
+session s4
+step s4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step itto	{ SET idle_in_transaction_session_timeout = '10ms'; SET transaction_timeout = '1s'; }
+
+session s5
+step s5_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step tito	{ SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '10ms'; }
+
+session s6
+step s6_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step s6_tt	{ SET statement_timeout = '1s'; SET transaction_timeout = '10ms'; }
+
+session checker
+step checker_sleep	{ SELECT pg_sleep(0.1); }
+step s3_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3'; }
+step s4_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4'; }
+step s5_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5'; }
+step s6_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6'; }
+
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +74,14 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# statement timeout expires first
+permutation stto s3_begin s3_sleep s3_check s3_abort
+# transaction timeout expires first, session s3 FATAL-out
+permutation tsto s3_begin s3_sleep s3_check
+# idle in transaction timeout expires first, session s4 FATAL-out
+permutation itto s4_begin checker_sleep s4_check
+# transaction timeout expires first, session s5 FATAL-out
+permutation tito s5_begin checker_sleep s5_check
+# transaction timeout can be schedule amid transaction, session s6 FATAL-out
+permutation s6_begin s6_tt checker_sleep s6_check
\ No newline at end of file
-- 
2.42.0



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-01-26 06:46  Andrey M. Borodin <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  1 sibling, 0 replies; 76+ messages in thread

From: Andrey M. Borodin @ 2024-01-26 06:46 UTC (permalink / raw)
  To: Peter Smith <[email protected]>; +Cc: Japin Li <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>



> On 26 Jan 2024, at 11:44, Andrey M. Borodin <[email protected]> wrote:
> 
> 
> 1. It’s unsafe for isaoltion tester to await transaction_timeout within a query. Usually it gets
> FATAL: terminating connection due to transaction timeout
> But if VM is a bit slow it can get occasional
> PQconsumeInput failed: server closed the connection unexpectedly
> So, currently all tests use “passive waiting”, in a session that will not timeout.


Oops, sorry, I’ve accidentally sent version without this fix.
Here it is.


Best regards, Andrey Borodin.


Attachments:

  [application/octet-stream] v24-0001-Introduce-transaction_timeout.patch (26.2K, ../../[email protected]/2-v24-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From ca3b48b20c987b0a557c4b6efa0297a539c45cb5 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v24] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Author: Japin Li <[email protected]>
Author: Junwang Zhao <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 35 ++++++++
 src/backend/access/transam/xact.c             |  4 +
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 33 +++++++-
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 +++
 src/backend/utils/misc/guc_tables.c           | 11 +++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/Makefile                   |  3 +
 src/test/isolation/expected/timeouts-long.out | 69 ++++++++++++++++
 src/test/isolation/expected/timeouts.out      | 79 ++++++++++++++++++-
 src/test/isolation/isolation_schedule         |  1 +
 src/test/isolation/specs/timeouts-long.spec   | 35 ++++++++
 src/test/isolation/specs/timeouts.spec        | 40 +++++++++-
 22 files changed, 329 insertions(+), 5 deletions(-)
 create mode 100644 src/test/isolation/expected/timeouts-long.out
 create mode 100644 src/test/isolation/specs/timeouts-long.spec

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 61038472c5a..bd099d06350 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9135,6 +9135,41 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 464858117e0..a124ba59330 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2139,6 +2139,10 @@ StartTransaction(void)
 	 */
 	s->state = TRANS_INPROGRESS;
 
+	/* Schedule transaction timeout */
+	if (TransactionTimeout > 0)
+		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 2c3099f76f1..c12fc6594ce 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 4ad96beb87a..b8234ef8e4b 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1a34bd3715f..af28f425ce6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3426,6 +3426,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,12 +4502,18 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else if (IsTransactionOrTransactionBlock())
 			{
@@ -4504,12 +4521,18 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else
 			{
@@ -4562,6 +4585,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5146,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 29f367a5e1c..3250d539e1c 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 88b03e8fa3c..f024b1a8497 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 1ad33671598..7797876d008 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 7fe58518d7d..0fb5ec648e4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2577,6 +2577,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index da10b43dac3..3b8992f0fbf 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -701,6 +701,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4e..3342971bd01 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index bc20a025ce4..c2076f979a3 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1252,6 +1252,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 11347ab1824..7d898c3b501 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0b01c1f0935..0445fbf61d7 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 4bc226e36cd..20d6fa652dc 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -429,6 +429,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 20e7cf72d0d..a5d8f078246 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/Makefile b/src/test/isolation/Makefile
index ade2256ed3a..91307e1a7e8 100644
--- a/src/test/isolation/Makefile
+++ b/src/test/isolation/Makefile
@@ -72,3 +72,6 @@ installcheck-prepared-txns: all temp-install
 
 check-prepared-txns: all temp-install
 	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule prepared-transactions prepared-transactions-cic
+
+check-timeouts: all temp-install
+	$(pg_isolation_regress_check) timeouts timeouts-long
diff --git a/src/test/isolation/expected/timeouts-long.out b/src/test/isolation/expected/timeouts-long.out
new file mode 100644
index 00000000000..26a6672c051
--- /dev/null
+++ b/src/test/isolation/expected/timeouts-long.out
@@ -0,0 +1,69 @@
+Parsed test spec with 3 sessions
+
+starting permutation: s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_check s7_abort
+step s7_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '1s';
+
+step s7_sleep: SELECT pg_sleep(0.6);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_commit_and_chain: COMMIT AND CHAIN;
+step s7_sleep: SELECT pg_sleep(0.6);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7';
+count
+-----
+    0
+(1 row)
+
+step s7_abort: ABORT;
+
+starting permutation: s8_begin s8_sleep s8_select_1 s8_check checker_sleep checker_sleep s8_check
+step s8_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '900ms';
+
+step s8_sleep: SELECT pg_sleep(0.6);
+pg_sleep
+--------
+        
+(1 row)
+
+step s8_select_1: SELECT 1;
+?column?
+--------
+       1
+(1 row)
+
+step s8_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s8';
+count
+-----
+    0
+(1 row)
+
+step checker_sleep: SELECT pg_sleep(0.3);
+pg_sleep
+--------
+        
+(1 row)
+
+step checker_sleep: SELECT pg_sleep(0.3);
+pg_sleep
+--------
+        
+(1 row)
+
+step s8_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s8';
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1cc..81a0016375b 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 7 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,80 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stto s3_begin s3_sleep s3_check s3_abort
+step stto: SET statement_timeout = '10ms'; SET transaction_timeout = '1s';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s3_sleep: SELECT pg_sleep(0.1);
+ERROR:  canceling statement due to statement timeout
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    1
+(1 row)
+
+step s3_abort: ABORT;
+
+starting permutation: tsto s3_begin checker_sleep s3_check
+step tsto: SET statement_timeout = '1s'; SET transaction_timeout = '10ms';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: itto s4_begin checker_sleep s4_check
+step itto: SET idle_in_transaction_session_timeout = '10ms'; SET transaction_timeout = '1s';
+step s4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s4_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: tito s5_begin checker_sleep s5_check
+step tito: SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '10ms';
+step s5_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s5_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: s6_begin s6_tt checker_sleep s6_check
+step s6_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s6_tt: SET statement_timeout = '1s'; SET transaction_timeout = '10ms';
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s6_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6';
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b2be88ead1d..86ef62bbcf6 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -89,6 +89,7 @@ test: sequence-ddl
 test: async-notify
 test: vacuum-no-cleanup-lock
 test: timeouts
+test: timeouts-long
 test: vacuum-concurrent-drop
 test: vacuum-conflict
 test: vacuum-skip-locked
diff --git a/src/test/isolation/specs/timeouts-long.spec b/src/test/isolation/specs/timeouts-long.spec
new file mode 100644
index 00000000000..ce2c9a43011
--- /dev/null
+++ b/src/test/isolation/specs/timeouts-long.spec
@@ -0,0 +1,35 @@
+# Tests for transaction timeout that require long wait times
+
+session s7
+step s7_begin
+{
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '1s';
+}
+step s7_commit_and_chain { COMMIT AND CHAIN; }
+step s7_sleep	{ SELECT pg_sleep(0.6); }
+step s7_abort	{ ABORT; }
+
+session s8
+step s8_begin
+{
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '900ms';
+}
+# to test that quick query does not restart transaction_timeout
+step s8_select_1 { SELECT 1; }
+step s8_sleep	{ SELECT pg_sleep(0.6); }
+
+session checker
+step checker_sleep	{ SELECT pg_sleep(0.3); }
+step s7_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7'; }
+step s8_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s8'; }
+
+# COMMIT AND CHAIN must restart transaction timeout
+permutation s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_check s7_abort
+# transaction timeout expires in presence of query flow, session s7 FATAL-out
+# this relatevely long sleeps are picked to ensure 300ms gap between check and timeouts firing
+# expected flow: timeouts is scheduled after s8_begin and fires approximately after checker_sleep (300ms before check)
+# possible buggy flow: timeout is schedules after s8_select_1 and fires 300ms after s8_check
+# to ensure this 300ms gap we need minimum transaction_timeout of 300ms
+permutation s8_begin s8_sleep s8_select_1 s8_check checker_sleep checker_sleep s8_check
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28d..c2cc5d8d37b 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -1,4 +1,4 @@
-# Simple tests for statement_timeout and lock_timeout features
+# Simple tests for statement_timeout, lock_timeout and transaction_timeout features
 
 setup
 {
@@ -27,6 +27,33 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session s3
+step s3_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step stto	{ SET statement_timeout = '10ms'; SET transaction_timeout = '1s'; }
+step tsto	{ SET statement_timeout = '1s'; SET transaction_timeout = '10ms'; }
+step s3_sleep	{ SELECT pg_sleep(0.1); }
+step s3_abort	{ ABORT; }
+
+session s4
+step s4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step itto	{ SET idle_in_transaction_session_timeout = '10ms'; SET transaction_timeout = '1s'; }
+
+session s5
+step s5_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step tito	{ SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '10ms'; }
+
+session s6
+step s6_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step s6_tt	{ SET statement_timeout = '1s'; SET transaction_timeout = '10ms'; }
+
+session checker
+step checker_sleep	{ SELECT pg_sleep(0.1); }
+step s3_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3'; }
+step s4_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4'; }
+step s5_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5'; }
+step s6_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6'; }
+
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +74,14 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# statement timeout expires first
+permutation stto s3_begin s3_sleep s3_check s3_abort
+# transaction timeout expires first, session s3 FATAL-out
+permutation tsto s3_begin checker_sleep s3_check
+# idle in transaction timeout expires first, session s4 FATAL-out
+permutation itto s4_begin checker_sleep s4_check
+# transaction timeout expires first, session s5 FATAL-out
+permutation tito s5_begin checker_sleep s5_check
+# transaction timeout can be schedule amid transaction, session s6 FATAL-out
+permutation s6_begin s6_tt checker_sleep s6_check
\ No newline at end of file
-- 
2.42.0



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-01-26 14:58  Japin Li <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  1 sibling, 1 reply; 76+ messages in thread

From: Japin Li @ 2024-01-26 14:58 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>


On Fri, 26 Jan 2024 at 14:44, Andrey M. Borodin <[email protected]> wrote:
>> On 22 Jan 2024, at 11:23, Peter Smith <[email protected]> wrote:
>>
>> Hi, This patch has a CF status of "Needs Review" [1], but it seems
>> there was a CFbot test failure last time it was run [2]. Please have a
>> look and post an updated version if necessary.
> Thanks Peter!
>

Thanks for updating the patch.  Here are some comments for v24.

+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement. But this limit is not
+        applied to prepared transactions.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
The sentence "But this limit is not applied to prepared transactions" is redundant,
since we have a paragraph to describe this later.

+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter than
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+

Since we are already try to disable the timeouts, should we try to disable
them even if they are equal.

+
+       <para>
+        Prepared transactions are not subject for this timeout.
+       </para>

Maybe wrap this with <note> is a good idea.

> I’ve inspected CI fails and they were caused by two different problems:
> 1. It’s unsafe for isaoltion tester to await transaction_timeout within a query. Usually it gets
> FATAL: terminating connection due to transaction timeout
> But if VM is a bit slow it can get occasional
> PQconsumeInput failed: server closed the connection unexpectedly
> So, currently all tests use “passive waiting”, in a session that will not timeout.
>
> 2. In some cases pg_sleep(0.1) were sleeping up to 200 ms. That was making s7 and s8 fail, because they rely on this margin.

I'm curious why this happened.

> I’ve separated these tests into different test timeouts-long and increased margin to 300ms. Now tests run horrible 2431 ms. Moreover I’m afraid that on buildfarm we can have much randomly-slower machines so this test might be excluded.
> This test checks COMMIT AND CHAIN and flow of small queries (Nik’s case).
>
> Also I’ve verified that every "enable_timeout_after(TRANSACTION_TIMEOUT)” and “disable_timeout(TRANSACTION_TIMEOUT)” is necessary and found that case of aborting "idle in transaction (aborted)” is not covered by tests. I’m not sure we need a test for this.

I see there is a test about idle_in_transaction_timeout and transaction_timeout.

Both of them only check the session, but don't check the reason, so we cannot
distinguish the reason they are terminated.  Right?

> Japin, Junwang, what do you think?

However, checking the reason on the timeout session may cause regression test
failed (as you point in 1), I don't strongly insist on it.

--
Best regards,
Japin Li.





^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-01-30 06:22  Andrey M. Borodin <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2024-01-30 06:22 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>



> On 26 Jan 2024, at 19:58, Japin Li <[email protected]> wrote:
> 
> Thanks for updating the patch.  Here are some comments for v24.
> 
> +       <para>
> +        Terminate any session that spans longer than the specified amount of
> +        time in transaction. The limit applies both to explicit transactions
> +        (started with <command>BEGIN</command>) and to implicitly started
> +        transaction corresponding to single statement. But this limit is not
> +        applied to prepared transactions.
> +        If this value is specified without units, it is taken as milliseconds.
> +        A value of zero (the default) disables the timeout.
> +       </para>
> The sentence "But this limit is not applied to prepared transactions" is redundant,
> since we have a paragraph to describe this later.
Fixed.
> 
> +
> +       <para>
> +        If <varname>transaction_timeout</varname> is shorter than
> +        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
> +        <varname>transaction_timeout</varname> will invalidate longer timeout.
> +       </para>
> +
> 
> Since we are already try to disable the timeouts, should we try to disable
> them even if they are equal.

Well, we disable timeouts on equality. Fixed docs.

> 
> +
> +       <para>
> +        Prepared transactions are not subject for this timeout.
> +       </para>
> 
> Maybe wrap this with <note> is a good idea.
Done.

> 
>> I’ve inspected CI fails and they were caused by two different problems:
>> 1. It’s unsafe for isaoltion tester to await transaction_timeout within a query. Usually it gets
>> FATAL: terminating connection due to transaction timeout
>> But if VM is a bit slow it can get occasional
>> PQconsumeInput failed: server closed the connection unexpectedly
>> So, currently all tests use “passive waiting”, in a session that will not timeout.
>> 
>> 2. In some cases pg_sleep(0.1) were sleeping up to 200 ms. That was making s7 and s8 fail, because they rely on this margin.
> 
> I'm curious why this happened.
I think pg_sleep() cannot provide guarantees on when next query will be executed. In our case we need that isolation tester see that sleep is over and continue in other session...

>> I’ve separated these tests into different test timeouts-long and increased margin to 300ms. Now tests run horrible 2431 ms. Moreover I’m afraid that on buildfarm we can have much randomly-slower machines so this test might be excluded.
>> This test checks COMMIT AND CHAIN and flow of small queries (Nik’s case).
>> 
>> Also I’ve verified that every "enable_timeout_after(TRANSACTION_TIMEOUT)” and “disable_timeout(TRANSACTION_TIMEOUT)” is necessary and found that case of aborting "idle in transaction (aborted)” is not covered by tests. I’m not sure we need a test for this.
> 
> I see there is a test about idle_in_transaction_timeout and transaction_timeout.
> 
> Both of them only check the session, but don't check the reason, so we cannot
> distinguish the reason they are terminated.  Right?
Yes.
> 
>> Japin, Junwang, what do you think?
> 
> However, checking the reason on the timeout session may cause regression test
> failed (as you point in 1), I don't strongly insist on it.

Indeed, if we check a reason of FATAL timeouts - we get flaky tests.


Best regards, Andrey Borodin.


Attachments:

  [application/octet-stream] v25-0001-Introduce-transaction_timeout.patch (26.1K, ../../[email protected]/2-v25-0001-Introduce-transaction_timeout.patch)
  download | inline diff:
From 5154fbc3377aa9a4025f04c90da61861ac558761 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Sun, 3 Dec 2023 23:18:00 +0500
Subject: [PATCH v25] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Author: Japin Li <[email protected]>
Author: Junwang Zhao <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 36 +++++++++
 src/backend/access/transam/xact.c             |  4 +
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 33 +++++++-
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 +++
 src/backend/utils/misc/guc_tables.c           | 11 +++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  2 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/Makefile                   |  3 +
 src/test/isolation/expected/timeouts-long.out | 69 ++++++++++++++++
 src/test/isolation/expected/timeouts.out      | 79 ++++++++++++++++++-
 src/test/isolation/isolation_schedule         |  1 +
 src/test/isolation/specs/timeouts-long.spec   | 35 ++++++++
 src/test/isolation/specs/timeouts.spec        | 40 +++++++++-
 22 files changed, 330 insertions(+), 5 deletions(-)
 create mode 100644 src/test/isolation/expected/timeouts-long.out
 create mode 100644 src/test/isolation/specs/timeouts-long.spec

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 61038472c5a..ddbfa9a631b 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9135,6 +9135,42 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to implicitly started
+        transaction corresponding to single statement.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter or eqaul to
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <note>
+        <para>
+         Prepared transactions are not subject for this timeout.
+        </para>
+       </note>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 464858117e0..a124ba59330 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2139,6 +2139,10 @@ StartTransaction(void)
 	 */
 	s->state = TRANS_INPROGRESS;
 
+	/* Schedule transaction timeout */
+	if (TransactionTimeout > 0)
+		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 2c3099f76f1..c12fc6594ce 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index e5977548fe2..1afcbfc052c 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1a34bd3715f..af28f425ce6 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3426,6 +3426,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -4491,12 +4502,18 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else if (IsTransactionOrTransactionBlock())
 			{
@@ -4504,12 +4521,18 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else
 			{
@@ -4562,6 +4585,9 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				if (get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5146,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 29f367a5e1c..3250d539e1c 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 88b03e8fa3c..f024b1a8497 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 1ad33671598..7797876d008 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 7fe58518d7d..0fb5ec648e4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2577,6 +2577,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index da10b43dac3..3b8992f0fbf 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -701,6 +701,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4e..3342971bd01 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,8 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	// TODO: AB: do we need spacial handling for this?
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a19443becd6..119cfbcf0f5 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1252,6 +1252,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 11347ab1824..7d898c3b501 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0b01c1f0935..0445fbf61d7 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 4bc226e36cd..20d6fa652dc 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -429,6 +429,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 20e7cf72d0d..a5d8f078246 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/Makefile b/src/test/isolation/Makefile
index ade2256ed3a..91307e1a7e8 100644
--- a/src/test/isolation/Makefile
+++ b/src/test/isolation/Makefile
@@ -72,3 +72,6 @@ installcheck-prepared-txns: all temp-install
 
 check-prepared-txns: all temp-install
 	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule prepared-transactions prepared-transactions-cic
+
+check-timeouts: all temp-install
+	$(pg_isolation_regress_check) timeouts timeouts-long
diff --git a/src/test/isolation/expected/timeouts-long.out b/src/test/isolation/expected/timeouts-long.out
new file mode 100644
index 00000000000..26a6672c051
--- /dev/null
+++ b/src/test/isolation/expected/timeouts-long.out
@@ -0,0 +1,69 @@
+Parsed test spec with 3 sessions
+
+starting permutation: s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_check s7_abort
+step s7_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '1s';
+
+step s7_sleep: SELECT pg_sleep(0.6);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_commit_and_chain: COMMIT AND CHAIN;
+step s7_sleep: SELECT pg_sleep(0.6);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7';
+count
+-----
+    0
+(1 row)
+
+step s7_abort: ABORT;
+
+starting permutation: s8_begin s8_sleep s8_select_1 s8_check checker_sleep checker_sleep s8_check
+step s8_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '900ms';
+
+step s8_sleep: SELECT pg_sleep(0.6);
+pg_sleep
+--------
+        
+(1 row)
+
+step s8_select_1: SELECT 1;
+?column?
+--------
+       1
+(1 row)
+
+step s8_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s8';
+count
+-----
+    0
+(1 row)
+
+step checker_sleep: SELECT pg_sleep(0.3);
+pg_sleep
+--------
+        
+(1 row)
+
+step checker_sleep: SELECT pg_sleep(0.3);
+pg_sleep
+--------
+        
+(1 row)
+
+step s8_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s8';
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1cc..81a0016375b 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 7 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,80 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stto s3_begin s3_sleep s3_check s3_abort
+step stto: SET statement_timeout = '10ms'; SET transaction_timeout = '1s';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s3_sleep: SELECT pg_sleep(0.1);
+ERROR:  canceling statement due to statement timeout
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    1
+(1 row)
+
+step s3_abort: ABORT;
+
+starting permutation: tsto s3_begin checker_sleep s3_check
+step tsto: SET statement_timeout = '1s'; SET transaction_timeout = '10ms';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: itto s4_begin checker_sleep s4_check
+step itto: SET idle_in_transaction_session_timeout = '10ms'; SET transaction_timeout = '1s';
+step s4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s4_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: tito s5_begin checker_sleep s5_check
+step tito: SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '10ms';
+step s5_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s5_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: s6_begin s6_tt checker_sleep s6_check
+step s6_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s6_tt: SET statement_timeout = '1s'; SET transaction_timeout = '10ms';
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s6_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6';
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b2be88ead1d..86ef62bbcf6 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -89,6 +89,7 @@ test: sequence-ddl
 test: async-notify
 test: vacuum-no-cleanup-lock
 test: timeouts
+test: timeouts-long
 test: vacuum-concurrent-drop
 test: vacuum-conflict
 test: vacuum-skip-locked
diff --git a/src/test/isolation/specs/timeouts-long.spec b/src/test/isolation/specs/timeouts-long.spec
new file mode 100644
index 00000000000..ce2c9a43011
--- /dev/null
+++ b/src/test/isolation/specs/timeouts-long.spec
@@ -0,0 +1,35 @@
+# Tests for transaction timeout that require long wait times
+
+session s7
+step s7_begin
+{
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '1s';
+}
+step s7_commit_and_chain { COMMIT AND CHAIN; }
+step s7_sleep	{ SELECT pg_sleep(0.6); }
+step s7_abort	{ ABORT; }
+
+session s8
+step s8_begin
+{
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '900ms';
+}
+# to test that quick query does not restart transaction_timeout
+step s8_select_1 { SELECT 1; }
+step s8_sleep	{ SELECT pg_sleep(0.6); }
+
+session checker
+step checker_sleep	{ SELECT pg_sleep(0.3); }
+step s7_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7'; }
+step s8_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s8'; }
+
+# COMMIT AND CHAIN must restart transaction timeout
+permutation s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_check s7_abort
+# transaction timeout expires in presence of query flow, session s7 FATAL-out
+# this relatevely long sleeps are picked to ensure 300ms gap between check and timeouts firing
+# expected flow: timeouts is scheduled after s8_begin and fires approximately after checker_sleep (300ms before check)
+# possible buggy flow: timeout is schedules after s8_select_1 and fires 300ms after s8_check
+# to ensure this 300ms gap we need minimum transaction_timeout of 300ms
+permutation s8_begin s8_sleep s8_select_1 s8_check checker_sleep checker_sleep s8_check
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28d..c2cc5d8d37b 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -1,4 +1,4 @@
-# Simple tests for statement_timeout and lock_timeout features
+# Simple tests for statement_timeout, lock_timeout and transaction_timeout features
 
 setup
 {
@@ -27,6 +27,33 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session s3
+step s3_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step stto	{ SET statement_timeout = '10ms'; SET transaction_timeout = '1s'; }
+step tsto	{ SET statement_timeout = '1s'; SET transaction_timeout = '10ms'; }
+step s3_sleep	{ SELECT pg_sleep(0.1); }
+step s3_abort	{ ABORT; }
+
+session s4
+step s4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step itto	{ SET idle_in_transaction_session_timeout = '10ms'; SET transaction_timeout = '1s'; }
+
+session s5
+step s5_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step tito	{ SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '10ms'; }
+
+session s6
+step s6_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step s6_tt	{ SET statement_timeout = '1s'; SET transaction_timeout = '10ms'; }
+
+session checker
+step checker_sleep	{ SELECT pg_sleep(0.1); }
+step s3_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3'; }
+step s4_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4'; }
+step s5_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5'; }
+step s6_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6'; }
+
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +74,14 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# statement timeout expires first
+permutation stto s3_begin s3_sleep s3_check s3_abort
+# transaction timeout expires first, session s3 FATAL-out
+permutation tsto s3_begin checker_sleep s3_check
+# idle in transaction timeout expires first, session s4 FATAL-out
+permutation itto s4_begin checker_sleep s4_check
+# transaction timeout expires first, session s5 FATAL-out
+permutation tito s5_begin checker_sleep s5_check
+# transaction timeout can be schedule amid transaction, session s6 FATAL-out
+permutation s6_begin s6_tt checker_sleep s6_check
\ No newline at end of file
-- 
2.42.0



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-01-31 09:27  Japin Li <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Japin Li @ 2024-01-31 09:27 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrey Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; [email protected] <[email protected]>


On Tue, 30 Jan 2024 at 14:22, Andrey M. Borodin <[email protected]> wrote:
>> On 26 Jan 2024, at 19:58, Japin Li <[email protected]> wrote:
>>
>> Thanks for updating the patch.  Here are some comments for v24.
>>
>> +       <para>
>> +        Terminate any session that spans longer than the specified amount of
>> +        time in transaction. The limit applies both to explicit transactions
>> +        (started with <command>BEGIN</command>) and to implicitly started
>> +        transaction corresponding to single statement. But this limit is not
>> +        applied to prepared transactions.
>> +        If this value is specified without units, it is taken as milliseconds.
>> +        A value of zero (the default) disables the timeout.
>> +       </para>
>> The sentence "But this limit is not applied to prepared transactions" is redundant,
>> since we have a paragraph to describe this later.
> Fixed.
>>
>> +
>> +       <para>
>> +        If <varname>transaction_timeout</varname> is shorter than
>> +        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
>> +        <varname>transaction_timeout</varname> will invalidate longer timeout.
>> +       </para>
>> +
>>
>> Since we are already try to disable the timeouts, should we try to disable
>> them even if they are equal.
>
> Well, we disable timeouts on equality. Fixed docs.
>
>>
>> +
>> +       <para>
>> +        Prepared transactions are not subject for this timeout.
>> +       </para>
>>
>> Maybe wrap this with <note> is a good idea.
> Done.
>

Thanks for updating the patch.  LGTM.

If there is no other objections, I'll change it to ready for committer
next Monday.





^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-01-31 09:57  Andrey Borodin <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey Borodin @ 2024-01-31 09:57 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrew Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; pgsql-hackers mailing list <[email protected]>



> On 31 Jan 2024, at 14:27, Japin Li <[email protected]> wrote:
> 
> LGTM.
> 
> If there is no other objections, I'll change it to ready for committer
> next Monday.

I think we have a quorum, so I decided to go ahead and flipped status to RfC. Thanks!


Best regards, Andrey Borodin.




^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-02-13 21:42  Alexander Korotkov <[email protected]>
  parent: Andrey Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Alexander Korotkov @ 2024-02-13 21:42 UTC (permalink / raw)
  To: Andrey Borodin <[email protected]>; +Cc: Japin Li <[email protected]>; Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrew Borodin <[email protected]>; Andres Freund <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; pgsql-hackers mailing list <[email protected]>

Hi!

On Wed, Jan 31, 2024 at 11:57 AM Andrey Borodin <[email protected]> wrote:
> > On 31 Jan 2024, at 14:27, Japin Li <[email protected]> wrote:
> >
> > LGTM.
> >
> > If there is no other objections, I'll change it to ready for committer
> > next Monday.
>
> I think we have a quorum, so I decided to go ahead and flipped status to RfC. Thanks!

I checked this patch.  Generally I look good.  I've slightly revised that.

I think there is one unaddressed concern by Andres Freund [1] about
the overhead of this patch by adding extra branches and function calls
in the case transaction_timeout is disabled.  I tried to measure the
overhead of this patch using a pgbench script containing 20 semicolons
(20 empty statements in 20 empty transactions).  I didn't manage to
find measurable overhead or change of performance profile (I used
XCode Instruments on my x86 MacBook).  One thing, which I still found
possible to do is to avoid unconditional calls to
get_timeout_active(TRANSACTION_TIMEOUT).  Instead I put responsibility
for disabling timeout after GUC disables the transaction_timeout
assign hook.

I removed the TODO comment from _doSetFixedOutputState().  I think
backup restore is the operation where slow commands and slow
transactions are expected, and it's natural to disable
transaction_timeout among other timeouts there.  And the existing
comment clarifies that.

Also I made some grammar fixes to docs and comments.

I'm going to push this if there are no objections.

Links.
1. https://www.postgresql.org/message-id/20221206011050.s6hapukjqha35hud%40alap3.anarazel.de

------
Regards,
Alexander Korotkov


Attachments:

  [application/octet-stream] 0001-Introduce-transaction_timeout-v26.patch (27.5K, ../../CAPpHfds=ByL_MEZunF7P+AaDJ7xv7LD8kHcV9awWZx5fOJZBRw@mail.gmail.com/2-0001-Introduce-transaction_timeout-v26.patch)
  download | inline diff:
From 01aa52ddf49f5f640f50e65e2a5716294c6b6e32 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email protected]>
Date: Tue, 13 Feb 2024 23:30:36 +0200
Subject: [PATCH] Introduce transaction_timeout

This commit adds timeout that is expected to be used as a prevention
of long-running queries. Any session within the transaction will be
terminated after spanning longer than this timeout.

However, this timeout is not applied to prepared transactions.
Only transactions with user connections are affected.

Author: Andrey Borodin <[email protected]>
Author: Japin Li <[email protected]>
Author: Junwang Zhao <[email protected]>
Reviewed-by: Nikolay Samokhvalov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Fujii Masao <[email protected]>
Reviewed-by: bt23nguyent <[email protected]>
Reviewed-by: Yuhang Qiu <[email protected]>

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 doc/src/sgml/config.sgml                      | 36 +++++++++
 src/backend/access/transam/xact.c             |  4 +
 src/backend/postmaster/autovacuum.c           |  2 +
 src/backend/storage/lmgr/proc.c               |  1 +
 src/backend/tcop/postgres.c                   | 46 ++++++++++-
 src/backend/utils/errcodes.txt                |  1 +
 src/backend/utils/init/globals.c              |  1 +
 src/backend/utils/init/postinit.c             | 10 +++
 src/backend/utils/misc/guc_tables.c           | 11 +++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/bin/pg_dump/pg_backup_archiver.c          |  1 +
 src/bin/pg_dump/pg_dump.c                     |  2 +
 src/bin/pg_rewind/libpq_source.c              |  1 +
 src/include/miscadmin.h                       |  1 +
 src/include/storage/proc.h                    |  1 +
 src/include/utils/guc_hooks.h                 |  1 +
 src/include/utils/timeout.h                   |  1 +
 src/test/isolation/Makefile                   |  3 +
 src/test/isolation/expected/timeouts-long.out | 69 ++++++++++++++++
 src/test/isolation/expected/timeouts.out      | 79 ++++++++++++++++++-
 src/test/isolation/isolation_schedule         |  1 +
 src/test/isolation/specs/timeouts-long.spec   | 35 ++++++++
 src/test/isolation/specs/timeouts.spec        | 40 +++++++++-
 23 files changed, 343 insertions(+), 5 deletions(-)
 create mode 100644 src/test/isolation/expected/timeouts-long.out
 create mode 100644 src/test/isolation/specs/timeouts-long.spec

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 61038472c5a..b47b3ffcaee 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9135,6 +9135,42 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-transaction-timeout" xreflabel="transaction_timeout">
+      <term><varname>transaction_timeout</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>transaction_timeout</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Terminate any session that spans longer than the specified amount of
+        time in the transaction. The limit applies both to explicit transactions
+        (started with <command>BEGIN</command>) and to an implicitly started
+        transaction corresponding to a single statement.
+        If this value is specified without units, it is taken as milliseconds.
+        A value of zero (the default) disables the timeout.
+       </para>
+
+       <para>
+        If <varname>transaction_timeout</varname> is shorter or equal to
+        <varname>idle_in_transaction_session_timeout</varname> or <varname>statement_timeout</varname>
+        <varname>transaction_timeout</varname> will invalidate the longer timeout.
+       </para>
+
+       <para>
+        Setting <varname>transaction_timeout</varname> in
+        <filename>postgresql.conf</filename> is not recommended because it would
+        affect all sessions.
+       </para>
+
+       <note>
+        <para>
+         Prepared transactions are not subject to this timeout.
+        </para>
+       </note>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-lock-timeout" xreflabel="lock_timeout">
       <term><varname>lock_timeout</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 464858117e0..a124ba59330 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -2139,6 +2139,10 @@ StartTransaction(void)
 	 */
 	s->state = TRANS_INPROGRESS;
 
+	/* Schedule transaction timeout */
+	if (TransactionTimeout > 0)
+		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
+
 	ShowTransactionState("StartTransaction");
 }
 
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 2c3099f76f1..c12fc6594ce 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -586,6 +586,7 @@ AutoVacLauncherMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
@@ -1591,6 +1592,7 @@ AutoVacWorkerMain(int argc, char *argv[])
 	 * regular maintenance from being executed.
 	 */
 	SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
+	SetConfigOption("transaction_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
 	SetConfigOption("idle_in_transaction_session_timeout", "0",
 					PGC_SUSET, PGC_S_OVERRIDE);
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index e5977548fe2..1afcbfc052c 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -59,6 +59,7 @@ int			DeadlockTimeout = 1000;
 int			StatementTimeout = 0;
 int			LockTimeout = 0;
 int			IdleInTransactionSessionTimeout = 0;
+int			TransactionTimeout = 0;
 int			IdleSessionTimeout = 0;
 bool		log_lock_waits = false;
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 1a34bd3715f..22f7b4eb2e2 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3426,6 +3426,17 @@ ProcessInterrupts(void)
 			IdleInTransactionSessionTimeoutPending = false;
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* As above, ignore the signal if the GUC has been reset to zero. */
+		if (TransactionTimeout > 0)
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		else
+			TransactionTimeoutPending = false;
+	}
+
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
@@ -3640,6 +3651,15 @@ check_log_stats(bool *newval, void **extra, GucSource source)
 	return true;
 }
 
+/* GUC assign hook for transaction_timeout */
+void
+assign_transaction_timeout(int newval, void *extra)
+{
+	if (TransactionTimeout <= 0 &&
+		get_timeout_active(TRANSACTION_TIMEOUT))
+		disable_timeout(TRANSACTION_TIMEOUT, false);
+}
+
 
 /*
  * set_debug_options --- apply "-d N" command line option
@@ -4491,12 +4511,18 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else if (IsTransactionOrTransactionBlock())
 			{
@@ -4504,12 +4530,18 @@ PostgresMain(const char *dbname, const char *username)
 				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
 
 				/* Start the idle-in-transaction timer */
-				if (IdleInTransactionSessionTimeout > 0)
+				if (IdleInTransactionSessionTimeout > 0
+					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
 				{
 					idle_in_transaction_timeout_enabled = true;
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Schedule or reschedule transaction timeout */
+				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
+					enable_timeout_after(TRANSACTION_TIMEOUT,
+										 TransactionTimeout);
 			}
 			else
 			{
@@ -4562,6 +4594,13 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				/*
+				 * If GUC is changed then it's handled in
+				 * assign_transaction_timeout().
+				 */
+				if (TransactionTimeout > 0 && get_timeout_active(TRANSACTION_TIMEOUT))
+					disable_timeout(TRANSACTION_TIMEOUT, false);
 			}
 
 			/* Report any recently-changed GUC options */
@@ -5120,7 +5159,8 @@ enable_statement_timeout(void)
 	/* must be within an xact */
 	Assert(xact_started);
 
-	if (StatementTimeout > 0)
+	if (StatementTimeout > 0
+		&& (StatementTimeout < TransactionTimeout || TransactionTimeout == 0))
 	{
 		if (!get_timeout_active(STATEMENT_TIMEOUT))
 			enable_timeout_after(STATEMENT_TIMEOUT, StatementTimeout);
diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt
index 29f367a5e1c..3250d539e1c 100644
--- a/src/backend/utils/errcodes.txt
+++ b/src/backend/utils/errcodes.txt
@@ -252,6 +252,7 @@ Section: Class 25 - Invalid Transaction State
 25P01    E    ERRCODE_NO_ACTIVE_SQL_TRANSACTION                              no_active_sql_transaction
 25P02    E    ERRCODE_IN_FAILED_SQL_TRANSACTION                              in_failed_sql_transaction
 25P03    E    ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT                    idle_in_transaction_session_timeout
+25P04    E    ERRCODE_TRANSACTION_TIMEOUT                                    transaction_timeout
 
 Section: Class 26 - Invalid SQL Statement Name
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 88b03e8fa3c..f024b1a8497 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -33,6 +33,7 @@ volatile sig_atomic_t ProcDiePending = false;
 volatile sig_atomic_t CheckClientConnectionPending = false;
 volatile sig_atomic_t ClientConnectionLost = false;
 volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
+volatile sig_atomic_t TransactionTimeoutPending = false;
 volatile sig_atomic_t IdleSessionTimeoutPending = false;
 volatile sig_atomic_t ProcSignalBarrierPending = false;
 volatile sig_atomic_t LogMemoryContextPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 1ad33671598..7797876d008 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -75,6 +75,7 @@ static void ShutdownPostgres(int code, Datum arg);
 static void StatementTimeoutHandler(void);
 static void LockTimeoutHandler(void);
 static void IdleInTransactionSessionTimeoutHandler(void);
+static void TransactionTimeoutHandler(void);
 static void IdleSessionTimeoutHandler(void);
 static void IdleStatsUpdateTimeoutHandler(void);
 static void ClientCheckTimeoutHandler(void);
@@ -764,6 +765,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		RegisterTimeout(LOCK_TIMEOUT, LockTimeoutHandler);
 		RegisterTimeout(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 						IdleInTransactionSessionTimeoutHandler);
+		RegisterTimeout(TRANSACTION_TIMEOUT, TransactionTimeoutHandler);
 		RegisterTimeout(IDLE_SESSION_TIMEOUT, IdleSessionTimeoutHandler);
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 		RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
@@ -1395,6 +1397,14 @@ LockTimeoutHandler(void)
 	kill(MyProcPid, SIGINT);
 }
 
+static void
+TransactionTimeoutHandler(void)
+{
+	TransactionTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 IdleInTransactionSessionTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 7fe58518d7d..70652f0a3fc 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2577,6 +2577,17 @@ struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Sets the maximum allowed time in a transaction with a session (not a prepared transaction)."),
+			gettext_noop("A value of 0 turns off the timeout."),
+			GUC_UNIT_MS
+		},
+		&TransactionTimeout,
+		0, 0, INT_MAX,
+		NULL, assign_transaction_timeout, NULL
+	},
+
 	{
 		{"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index da10b43dac3..3b8992f0fbf 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -701,6 +701,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #statement_timeout = 0				# in milliseconds, 0 is disabled
+#transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0			# in milliseconds, 0 is disabled
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 256d1e35a4e..d97ebaff5b8 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3115,6 +3115,7 @@ _doSetFixedOutputState(ArchiveHandle *AH)
 	ahprintf(AH, "SET statement_timeout = 0;\n");
 	ahprintf(AH, "SET lock_timeout = 0;\n");
 	ahprintf(AH, "SET idle_in_transaction_session_timeout = 0;\n");
+	ahprintf(AH, "SET transaction_timeout = 0;\n");
 
 	/* Select the correct character set encoding */
 	ahprintf(AH, "SET client_encoding = '%s';\n",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 348748bae53..12b487a12f5 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1252,6 +1252,8 @@ setup_connection(Archive *AH, const char *dumpencoding,
 		ExecuteSqlStatement(AH, "SET lock_timeout = 0");
 	if (AH->remoteVersion >= 90600)
 		ExecuteSqlStatement(AH, "SET idle_in_transaction_session_timeout = 0");
+	if (AH->remoteVersion >= 170000)
+		ExecuteSqlStatement(AH, "SET transaction_timeout = 0");
 
 	/*
 	 * Quote all identifiers, if requested.
diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c
index 11347ab1824..7d898c3b501 100644
--- a/src/bin/pg_rewind/libpq_source.c
+++ b/src/bin/pg_rewind/libpq_source.c
@@ -117,6 +117,7 @@ init_libpq_conn(PGconn *conn)
 	run_simple_command(conn, "SET statement_timeout = 0");
 	run_simple_command(conn, "SET lock_timeout = 0");
 	run_simple_command(conn, "SET idle_in_transaction_session_timeout = 0");
+	run_simple_command(conn, "SET transaction_timeout = 0");
 
 	/*
 	 * we don't intend to do any updates, put the connection in read-only mode
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 0b01c1f0935..0445fbf61d7 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -91,6 +91,7 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending;
 extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending;
+extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending;
 extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending;
 extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 4bc226e36cd..20d6fa652dc 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -429,6 +429,7 @@ extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
 extern PGDLLIMPORT int LockTimeout;
 extern PGDLLIMPORT int IdleInTransactionSessionTimeout;
+extern PGDLLIMPORT int TransactionTimeout;
 extern PGDLLIMPORT int IdleSessionTimeout;
 extern PGDLLIMPORT bool log_lock_waits;
 
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 5300c44f3b0..339c490300e 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -155,6 +155,7 @@ extern void assign_timezone_abbreviations(const char *newval, void *extra);
 extern bool check_transaction_deferrable(bool *newval, void **extra, GucSource source);
 extern bool check_transaction_isolation(int *newval, void **extra, GucSource source);
 extern bool check_transaction_read_only(bool *newval, void **extra, GucSource source);
+extern void assign_transaction_timeout(int newval, void *extra);
 extern const char *show_unix_socket_permissions(void);
 extern bool check_wal_buffers(int *newval, void **extra, GucSource source);
 extern bool check_wal_consistency_checking(char **newval, void **extra,
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 20e7cf72d0d..a5d8f078246 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -31,6 +31,7 @@ typedef enum TimeoutId
 	STANDBY_TIMEOUT,
 	STANDBY_LOCK_TIMEOUT,
 	IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
+	TRANSACTION_TIMEOUT,
 	IDLE_SESSION_TIMEOUT,
 	IDLE_STATS_UPDATE_TIMEOUT,
 	CLIENT_CONNECTION_CHECK_TIMEOUT,
diff --git a/src/test/isolation/Makefile b/src/test/isolation/Makefile
index ade2256ed3a..91307e1a7e8 100644
--- a/src/test/isolation/Makefile
+++ b/src/test/isolation/Makefile
@@ -72,3 +72,6 @@ installcheck-prepared-txns: all temp-install
 
 check-prepared-txns: all temp-install
 	$(pg_isolation_regress_check) --schedule=$(srcdir)/isolation_schedule prepared-transactions prepared-transactions-cic
+
+check-timeouts: all temp-install
+	$(pg_isolation_regress_check) timeouts timeouts-long
diff --git a/src/test/isolation/expected/timeouts-long.out b/src/test/isolation/expected/timeouts-long.out
new file mode 100644
index 00000000000..26a6672c051
--- /dev/null
+++ b/src/test/isolation/expected/timeouts-long.out
@@ -0,0 +1,69 @@
+Parsed test spec with 3 sessions
+
+starting permutation: s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_check s7_abort
+step s7_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '1s';
+
+step s7_sleep: SELECT pg_sleep(0.6);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_commit_and_chain: COMMIT AND CHAIN;
+step s7_sleep: SELECT pg_sleep(0.6);
+pg_sleep
+--------
+        
+(1 row)
+
+step s7_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7';
+count
+-----
+    0
+(1 row)
+
+step s7_abort: ABORT;
+
+starting permutation: s8_begin s8_sleep s8_select_1 s8_check checker_sleep checker_sleep s8_check
+step s8_begin: 
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '900ms';
+
+step s8_sleep: SELECT pg_sleep(0.6);
+pg_sleep
+--------
+        
+(1 row)
+
+step s8_select_1: SELECT 1;
+?column?
+--------
+       1
+(1 row)
+
+step s8_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s8';
+count
+-----
+    0
+(1 row)
+
+step checker_sleep: SELECT pg_sleep(0.3);
+pg_sleep
+--------
+        
+(1 row)
+
+step checker_sleep: SELECT pg_sleep(0.3);
+pg_sleep
+--------
+        
+(1 row)
+
+step s8_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s8';
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 9328676f1cc..81a0016375b 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 2 sessions
+Parsed test spec with 7 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,3 +79,80 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
+
+starting permutation: stto s3_begin s3_sleep s3_check s3_abort
+step stto: SET statement_timeout = '10ms'; SET transaction_timeout = '1s';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s3_sleep: SELECT pg_sleep(0.1);
+ERROR:  canceling statement due to statement timeout
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    1
+(1 row)
+
+step s3_abort: ABORT;
+
+starting permutation: tsto s3_begin checker_sleep s3_check
+step tsto: SET statement_timeout = '1s'; SET transaction_timeout = '10ms';
+step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: itto s4_begin checker_sleep s4_check
+step itto: SET idle_in_transaction_session_timeout = '10ms'; SET transaction_timeout = '1s';
+step s4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s4_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: tito s5_begin checker_sleep s5_check
+step tito: SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '10ms';
+step s5_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s5_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5';
+count
+-----
+    0
+(1 row)
+
+
+starting permutation: s6_begin s6_tt checker_sleep s6_check
+step s6_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
+step s6_tt: SET statement_timeout = '1s'; SET transaction_timeout = '10ms';
+step checker_sleep: SELECT pg_sleep(0.1);
+pg_sleep
+--------
+        
+(1 row)
+
+step s6_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6';
+count
+-----
+    0
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index b2be88ead1d..86ef62bbcf6 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -89,6 +89,7 @@ test: sequence-ddl
 test: async-notify
 test: vacuum-no-cleanup-lock
 test: timeouts
+test: timeouts-long
 test: vacuum-concurrent-drop
 test: vacuum-conflict
 test: vacuum-skip-locked
diff --git a/src/test/isolation/specs/timeouts-long.spec b/src/test/isolation/specs/timeouts-long.spec
new file mode 100644
index 00000000000..ce2c9a43011
--- /dev/null
+++ b/src/test/isolation/specs/timeouts-long.spec
@@ -0,0 +1,35 @@
+# Tests for transaction timeout that require long wait times
+
+session s7
+step s7_begin
+{
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '1s';
+}
+step s7_commit_and_chain { COMMIT AND CHAIN; }
+step s7_sleep	{ SELECT pg_sleep(0.6); }
+step s7_abort	{ ABORT; }
+
+session s8
+step s8_begin
+{
+    BEGIN ISOLATION LEVEL READ COMMITTED;
+    SET transaction_timeout = '900ms';
+}
+# to test that quick query does not restart transaction_timeout
+step s8_select_1 { SELECT 1; }
+step s8_sleep	{ SELECT pg_sleep(0.6); }
+
+session checker
+step checker_sleep	{ SELECT pg_sleep(0.3); }
+step s7_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s7'; }
+step s8_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s8'; }
+
+# COMMIT AND CHAIN must restart transaction timeout
+permutation s7_begin s7_sleep s7_commit_and_chain s7_sleep s7_check s7_abort
+# transaction timeout expires in presence of query flow, session s7 FATAL-out
+# this relatevely long sleeps are picked to ensure 300ms gap between check and timeouts firing
+# expected flow: timeouts is scheduled after s8_begin and fires approximately after checker_sleep (300ms before check)
+# possible buggy flow: timeout is schedules after s8_select_1 and fires 300ms after s8_check
+# to ensure this 300ms gap we need minimum transaction_timeout of 300ms
+permutation s8_begin s8_sleep s8_select_1 s8_check checker_sleep checker_sleep s8_check
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c747b4ae28d..c2cc5d8d37b 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -1,4 +1,4 @@
-# Simple tests for statement_timeout and lock_timeout features
+# Simple tests for statement_timeout, lock_timeout and transaction_timeout features
 
 setup
 {
@@ -27,6 +27,33 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
+session s3
+step s3_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step stto	{ SET statement_timeout = '10ms'; SET transaction_timeout = '1s'; }
+step tsto	{ SET statement_timeout = '1s'; SET transaction_timeout = '10ms'; }
+step s3_sleep	{ SELECT pg_sleep(0.1); }
+step s3_abort	{ ABORT; }
+
+session s4
+step s4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step itto	{ SET idle_in_transaction_session_timeout = '10ms'; SET transaction_timeout = '1s'; }
+
+session s5
+step s5_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step tito	{ SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '10ms'; }
+
+session s6
+step s6_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
+step s6_tt	{ SET statement_timeout = '1s'; SET transaction_timeout = '10ms'; }
+
+session checker
+step checker_sleep	{ SELECT pg_sleep(0.1); }
+step s3_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3'; }
+step s4_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4'; }
+step s5_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5'; }
+step s6_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6'; }
+
+
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -47,3 +74,14 @@ permutation wrtbl lto update(*)
 permutation wrtbl lsto update(*)
 # statement timeout expires first, row-level lock
 permutation wrtbl slto update(*)
+
+# statement timeout expires first
+permutation stto s3_begin s3_sleep s3_check s3_abort
+# transaction timeout expires first, session s3 FATAL-out
+permutation tsto s3_begin checker_sleep s3_check
+# idle in transaction timeout expires first, session s4 FATAL-out
+permutation itto s4_begin checker_sleep s4_check
+# transaction timeout expires first, session s5 FATAL-out
+permutation tito s5_begin checker_sleep s5_check
+# transaction timeout can be schedule amid transaction, session s6 FATAL-out
+permutation s6_begin s6_tt checker_sleep s6_check
\ No newline at end of file
-- 
2.39.3 (Apple Git-145)



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-02-15 23:08  Andres Freund <[email protected]>
  parent: Alexander Korotkov <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andres Freund @ 2024-02-15 23:08 UTC (permalink / raw)
  To: Alexander Korotkov <[email protected]>; +Cc: Andrey Borodin <[email protected]>; Japin Li <[email protected]>; Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrew Borodin <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; pgsql-hackers mailing list <[email protected]>

Hi,

On 2024-02-13 23:42:35 +0200, Alexander Korotkov wrote:
> diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
> index 464858117e0..a124ba59330 100644
> --- a/src/backend/access/transam/xact.c
> +++ b/src/backend/access/transam/xact.c
> @@ -2139,6 +2139,10 @@ StartTransaction(void)
>  	 */
>  	s->state = TRANS_INPROGRESS;
>  
> +	/* Schedule transaction timeout */
> +	if (TransactionTimeout > 0)
> +		enable_timeout_after(TRANSACTION_TIMEOUT, TransactionTimeout);
> +
>  	ShowTransactionState("StartTransaction");
>  }

Isn't it a problem that all uses of StartTransaction() trigger a timeout, but
transaction commit/abort don't?  What if e.g. logical replication apply starts
a transaction, commits it, and then goes idle? The timer would still be
active, afaict?

I don't think it works well to enable timeouts in xact.c and to disable them
in PostgresMain().


> @@ -4491,12 +4511,18 @@ PostgresMain(const char *dbname, const char *username)
>  				pgstat_report_activity(STATE_IDLEINTRANSACTION_ABORTED, NULL);
>  
>  				/* Start the idle-in-transaction timer */
> -				if (IdleInTransactionSessionTimeout > 0)
> +				if (IdleInTransactionSessionTimeout > 0
> +					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
>  				{
>  					idle_in_transaction_timeout_enabled = true;
>  					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
>  										 IdleInTransactionSessionTimeout);
>  				}
> +
> +				/* Schedule or reschedule transaction timeout */
> +				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
> +					enable_timeout_after(TRANSACTION_TIMEOUT,
> +										 TransactionTimeout);
>  			}
>  			else if (IsTransactionOrTransactionBlock())
>  			{
> @@ -4504,12 +4530,18 @@ PostgresMain(const char *dbname, const char *username)
>  				pgstat_report_activity(STATE_IDLEINTRANSACTION, NULL);
>  
>  				/* Start the idle-in-transaction timer */
> -				if (IdleInTransactionSessionTimeout > 0)
> +				if (IdleInTransactionSessionTimeout > 0
> +					&& (IdleInTransactionSessionTimeout < TransactionTimeout || TransactionTimeout == 0))
>  				{
>  					idle_in_transaction_timeout_enabled = true;
>  					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
>  										 IdleInTransactionSessionTimeout);
>  				}
> +
> +				/* Schedule or reschedule transaction timeout */
> +				if (TransactionTimeout > 0 && !get_timeout_active(TRANSACTION_TIMEOUT))
> +					enable_timeout_after(TRANSACTION_TIMEOUT,
> +										 TransactionTimeout);
>  			}
>  			else
>  			{

Why do we need to do anything in these cases if the timer is started in
StartTransaction()?


> new file mode 100644
> index 00000000000..ce2c9a43011
> --- /dev/null
> +++ b/src/test/isolation/specs/timeouts-long.spec
> @@ -0,0 +1,35 @@
> +# Tests for transaction timeout that require long wait times
> +
> +session s7
> +step s7_begin
> +{
> +    BEGIN ISOLATION LEVEL READ COMMITTED;
> +    SET transaction_timeout = '1s';
> +}
> +step s7_commit_and_chain { COMMIT AND CHAIN; }
> +step s7_sleep	{ SELECT pg_sleep(0.6); }
> +step s7_abort	{ ABORT; }
> +
> +session s8
> +step s8_begin
> +{
> +    BEGIN ISOLATION LEVEL READ COMMITTED;
> +    SET transaction_timeout = '900ms';
> +}
> +# to test that quick query does not restart transaction_timeout
> +step s8_select_1 { SELECT 1; }
> +step s8_sleep	{ SELECT pg_sleep(0.6); }
> +
> +session checker
> +step checker_sleep	{ SELECT pg_sleep(0.3); }

Isn't this test going to be very fragile on busy / slow machines? What if the
pg_sleep() takes one second, because there were other tasks to schedule?  I'd
be surprised if this didn't fail under valgrind, for example.


Greetings,

Andres Freund






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-02-18 19:16  Andrey M. Borodin <[email protected]>
  parent: Andres Freund <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2024-02-18 19:16 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Alexander Korotkov <[email protected]>; Japin Li <[email protected]>; Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrew Borodin <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; pgsql-hackers mailing list <[email protected]>

Alexander, thanks for pushing this! This is small but very awaited feature.

> On 16 Feb 2024, at 02:08, Andres Freund <[email protected]> wrote:
> 
> Isn't this test going to be very fragile on busy / slow machines? What if the
> pg_sleep() takes one second, because there were other tasks to schedule?  I'd
> be surprised if this didn't fail under valgrind, for example.

Even more robust tests that were bullet-proof in CI previously exhibited some failures on buildfarm. Currently there are 5 failures through this weekend.
Failing tests are testing interaction of idle_in_transaction_session_timeout vs transaction_timeout(5), and rescheduling transaction_timeout(6).
Symptoms:

[0] transaction timeout occurs when it is being scheduled. Seems like SET was running to long.
 step s6_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
 step s6_tt: SET statement_timeout = '1s'; SET transaction_timeout = '10ms';
+s6: FATAL:  terminating connection due to transaction timeout
 step checker_sleep: SELECT pg_sleep(0.1);

[1] transaction timeout 10ms is not detected after 1s
step s6_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6';
 count
 -----
-    0
+    1

[2] transaction timeout is not detected in both session 5 and session 6.

So far not signle animal reported failures twice, so it's hard to say anything about frequency. But it seems to be significant source of failures.

So far I have these ideas:

1. Remove test sessions 5 and 6. But it seems a little strange that session 3 did  not fail at all (it is testing interaction of statement_timeout and transaction_timeout). This test is very similar to test sessiont 5...
2. Increase wait times.
step checker_sleep	{ SELECT pg_sleep(0.1); }
Seems not enough to observe backend timed out from pg_stat_activity. But this won't help from [0].
3. Reuse waiting INJECTION_POINT from [3] to make timeout tests deterministic and safe from race conditions. With waiting injection points we can wait as much as needed in current environment.

Any advices are welcome.


Best regards, Andrey Borodin.


[0] https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=tamandua&dt=2024-02-16%2020%3A06%3A51
[1] https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=kestrel&dt=2024-02-16%2001%3A45%3A10
[2] https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=fairywren&dt=2024-02-17%2001%3A55%3A45
[3] https://www.postgresql.org/message-id/[email protected]





^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-02-19 09:14  Andrey M. Borodin <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2024-02-19 09:14 UTC (permalink / raw)
  To: Alexander Korotkov <[email protected]>; +Cc: Japin Li <[email protected]>; Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrew Borodin <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; pgsql-hackers mailing list <[email protected]>; Andres Freund <[email protected]>



> On 18 Feb 2024, at 22:16, Andrey M. Borodin <[email protected]> wrote:
> 
> But it seems a little strange that session 3 did  not fail at all
It was only coincidence. Any test that verifies FATALing out in 100ms can fail, see new failure here [0].

In a nearby thread Michael is proposing injections points that can wait and be awaken. So I propose following course of action:
1. Remove all tests that involve pg_stat_activity test of FATALed session (any permutation with checker_sleep step)
2. Add idle_in_transaction_session_timeout, statement_timeout and transaction_timeout tests when injection points features get committed.

Alexander, what do you think?

Best regards, Andrey Borodin.


[0] https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=grassquit&dt=2024-02-18%2022%3A23%3A45





^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-02-19 10:17  Japin Li <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Japin Li @ 2024-02-19 10:17 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: Alexander Korotkov <[email protected]>; Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrew Borodin <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; pgsql-hackers mailing list <[email protected]>; Andres Freund <[email protected]>


On Mon, 19 Feb 2024 at 17:14, Andrey M. Borodin <[email protected]> wrote:
>> On 18 Feb 2024, at 22:16, Andrey M. Borodin <[email protected]> wrote:
>>
>> But it seems a little strange that session 3 did  not fail at all
> It was only coincidence. Any test that verifies FATALing out in 100ms can fail, see new failure here [0].
>
> In a nearby thread Michael is proposing injections points that can wait and be awaken. So I propose following course of action:
> 1. Remove all tests that involve pg_stat_activity test of FATALed session (any permutation with checker_sleep step)
> 2. Add idle_in_transaction_session_timeout, statement_timeout and transaction_timeout tests when injection points features get committed.
>

+1

> Alexander, what do you think?
>






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-02-22 17:23  Andrey M. Borodin <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2024-02-22 17:23 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: Alexander Korotkov <[email protected]>; Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrew Borodin <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; pgsql-hackers mailing list <[email protected]>; Andres Freund <[email protected]>



> On 19 Feb 2024, at 15:17, Japin Li <[email protected]> wrote:
> 
> 
> +1

PFA patch set of 4 patches:
1. remove all potential flaky tests. BTW recently we had a bingo when 3 of them failed together [0]
2-3. waiting injection points patchset by Michael Paquier, intact v2 from nearby thread.
4. prototype of simple TAP tests for timeouts.

I did not add a test for statement_timeout, because it still have good coverage in isolation tests. But added test for idle_sessoin_timeout.
Maybe these tests could be implemented with NOTICE injection points (not requiring steps 2-3), but I'm afraid that they might be flaky too: FATALed connection might not send information necesary for test success (we will see something like "PQconsumeInput failed: server closed the connection unexpectedly" as in [1]).


Best regards, Andrey Borodin.

[0] https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=tamandua&dt=2024-02-20%2010%3A20%3A13
[1] https://www.postgresql.org/message-id/flat/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mai...



Attachments:

  [application/octet-stream] 0001-Remove-flacky-isolation-tests-for-timeouts.patch (6.2K, ../../[email protected]/2-0001-Remove-flacky-isolation-tests-for-timeouts.patch)
  download | inline diff:
From 0107045aa2ea699b138b5586b0bffb2f46bd3c06 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Wed, 21 Feb 2024 17:29:07 +0300
Subject: [PATCH 1/4] Remove flacky isolation tests for timeouts

51efe38cb92f introduced bunch of tests for idle_in_transaction_session_timeout,
transaction_timeout and statement_timeout. These tests were too flacky on
some slow buildfarm machines, so we plan to replace them with TAP
tests using injection points. This commit remove tests that were flacky.

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 src/test/isolation/expected/timeouts.out | 79 +-----------------------
 src/test/isolation/specs/timeouts.spec   | 40 +-----------
 2 files changed, 2 insertions(+), 117 deletions(-)

diff --git a/src/test/isolation/expected/timeouts.out b/src/test/isolation/expected/timeouts.out
index 81a0016375..9328676f1c 100644
--- a/src/test/isolation/expected/timeouts.out
+++ b/src/test/isolation/expected/timeouts.out
@@ -1,4 +1,4 @@
-Parsed test spec with 7 sessions
+Parsed test spec with 2 sessions
 
 starting permutation: rdtbl sto locktbl
 step rdtbl: SELECT * FROM accounts;
@@ -79,80 +79,3 @@ step slto: SET lock_timeout = '10s'; SET statement_timeout = '10ms';
 step update: DELETE FROM accounts WHERE accountid = 'checking'; <waiting ...>
 step update: <... completed>
 ERROR:  canceling statement due to statement timeout
-
-starting permutation: stto s3_begin s3_sleep s3_check s3_abort
-step stto: SET statement_timeout = '10ms'; SET transaction_timeout = '1s';
-step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step s3_sleep: SELECT pg_sleep(0.1);
-ERROR:  canceling statement due to statement timeout
-step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
-count
------
-    1
-(1 row)
-
-step s3_abort: ABORT;
-
-starting permutation: tsto s3_begin checker_sleep s3_check
-step tsto: SET statement_timeout = '1s'; SET transaction_timeout = '10ms';
-step s3_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step checker_sleep: SELECT pg_sleep(0.1);
-pg_sleep
---------
-        
-(1 row)
-
-step s3_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3';
-count
------
-    0
-(1 row)
-
-
-starting permutation: itto s4_begin checker_sleep s4_check
-step itto: SET idle_in_transaction_session_timeout = '10ms'; SET transaction_timeout = '1s';
-step s4_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step checker_sleep: SELECT pg_sleep(0.1);
-pg_sleep
---------
-        
-(1 row)
-
-step s4_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4';
-count
------
-    0
-(1 row)
-
-
-starting permutation: tito s5_begin checker_sleep s5_check
-step tito: SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '10ms';
-step s5_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step checker_sleep: SELECT pg_sleep(0.1);
-pg_sleep
---------
-        
-(1 row)
-
-step s5_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5';
-count
------
-    0
-(1 row)
-
-
-starting permutation: s6_begin s6_tt checker_sleep s6_check
-step s6_begin: BEGIN ISOLATION LEVEL READ COMMITTED;
-step s6_tt: SET statement_timeout = '1s'; SET transaction_timeout = '10ms';
-step checker_sleep: SELECT pg_sleep(0.1);
-pg_sleep
---------
-        
-(1 row)
-
-step s6_check: SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6';
-count
------
-    0
-(1 row)
-
diff --git a/src/test/isolation/specs/timeouts.spec b/src/test/isolation/specs/timeouts.spec
index c2cc5d8d37..a0eec49c38 100644
--- a/src/test/isolation/specs/timeouts.spec
+++ b/src/test/isolation/specs/timeouts.spec
@@ -27,33 +27,6 @@ step locktbl	{ LOCK TABLE accounts; }
 step update	{ DELETE FROM accounts WHERE accountid = 'checking'; }
 teardown	{ ABORT; }
 
-session s3
-step s3_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
-step stto	{ SET statement_timeout = '10ms'; SET transaction_timeout = '1s'; }
-step tsto	{ SET statement_timeout = '1s'; SET transaction_timeout = '10ms'; }
-step s3_sleep	{ SELECT pg_sleep(0.1); }
-step s3_abort	{ ABORT; }
-
-session s4
-step s4_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
-step itto	{ SET idle_in_transaction_session_timeout = '10ms'; SET transaction_timeout = '1s'; }
-
-session s5
-step s5_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
-step tito	{ SET idle_in_transaction_session_timeout = '1s'; SET transaction_timeout = '10ms'; }
-
-session s6
-step s6_begin	{ BEGIN ISOLATION LEVEL READ COMMITTED; }
-step s6_tt	{ SET statement_timeout = '1s'; SET transaction_timeout = '10ms'; }
-
-session checker
-step checker_sleep	{ SELECT pg_sleep(0.1); }
-step s3_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s3'; }
-step s4_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s4'; }
-step s5_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s5'; }
-step s6_check	{ SELECT count(*) FROM pg_stat_activity WHERE application_name = 'isolation/timeouts/s6'; }
-
-
 # It's possible that the isolation tester will not observe the final
 # steps as "waiting", thanks to the relatively short timeouts we use.
 # We can ensure consistent test output by marking those steps with (*).
@@ -73,15 +46,4 @@ 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(*)
-
-# statement timeout expires first
-permutation stto s3_begin s3_sleep s3_check s3_abort
-# transaction timeout expires first, session s3 FATAL-out
-permutation tsto s3_begin checker_sleep s3_check
-# idle in transaction timeout expires first, session s4 FATAL-out
-permutation itto s4_begin checker_sleep s4_check
-# transaction timeout expires first, session s5 FATAL-out
-permutation tito s5_begin checker_sleep s5_check
-# transaction timeout can be schedule amid transaction, session s6 FATAL-out
-permutation s6_begin s6_tt checker_sleep s6_check
\ No newline at end of file
+permutation wrtbl slto update(*)
\ No newline at end of file
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] 0004-Add-timeouts-TAP-tests.patch (6.5K, ../../[email protected]/3-0004-Add-timeouts-TAP-tests.patch)
  download | inline diff:
From 615c1bb798445886c54ece062db397146d7038fe Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Wed, 21 Feb 2024 19:34:26 +0300
Subject: [PATCH 4/4] Add timeouts TAP tests

These tests verify that transaction_timeout, idle_session_timeout
and idle_in_transaction_session_timeout work as expected.
---
 src/backend/tcop/postgres.c                  |  10 ++
 src/test/modules/test_misc/Makefile          |   4 +
 src/test/modules/test_misc/meson.build       |   4 +
 src/test/modules/test_misc/t/005_timeouts.pl | 104 +++++++++++++++++++
 4 files changed, 122 insertions(+)
 create mode 100644 src/test/modules/test_misc/t/005_timeouts.pl

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 2c63b7875a..7a9f650397 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -72,6 +72,7 @@
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
 #include "utils/guc_hooks.h"
+#include "utils/injection_point.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
@@ -3411,9 +3412,12 @@ ProcessInterrupts(void)
 		 * interrupt.
 		 */
 		if (IdleInTransactionSessionTimeout > 0)
+		{
+			INJECTION_POINT("IdleInTransactionSessionTimeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT),
 					 errmsg("terminating connection due to idle-in-transaction timeout")));
+		}
 		else
 			IdleInTransactionSessionTimeoutPending = false;
 	}
@@ -3422,9 +3426,12 @@ ProcessInterrupts(void)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
 		if (TransactionTimeout > 0)
+		{
+			INJECTION_POINT("TransactionTimeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
 					 errmsg("terminating connection due to transaction timeout")));
+		}
 		else
 			TransactionTimeoutPending = false;
 	}
@@ -3433,9 +3440,12 @@ ProcessInterrupts(void)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
 		if (IdleSessionTimeout > 0)
+		{
+			INJECTION_POINT("IdleSessionTimeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_IDLE_SESSION_TIMEOUT),
 					 errmsg("terminating connection due to idle-session timeout")));
+		}
 		else
 			IdleSessionTimeoutPending = false;
 	}
diff --git a/src/test/modules/test_misc/Makefile b/src/test/modules/test_misc/Makefile
index 39c6c2014a..a958d156f4 100644
--- a/src/test/modules/test_misc/Makefile
+++ b/src/test/modules/test_misc/Makefile
@@ -2,6 +2,10 @@
 
 TAP_TESTS = 1
 
+EXTRA_INSTALL=src/test/modules/injection_points
+
+export enable_injection_points enable_injection_points
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 964d95db26..df2913e893 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -5,11 +5,15 @@ tests += {
   'sd': meson.current_source_dir(),
   'bd': meson.current_build_dir(),
   'tap': {
+    'env': {
+       'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+    },
     'tests': [
       't/001_constraint_validation.pl',
       't/002_tablespace.pl',
       't/003_check_guc.pl',
       't/004_io_direct.pl',
+      't/005_timeouts.pl'
     ],
   },
 }
diff --git a/src/test/modules/test_misc/t/005_timeouts.pl b/src/test/modules/test_misc/t/005_timeouts.pl
new file mode 100644
index 0000000000..5d940e4441
--- /dev/null
+++ b/src/test/modules/test_misc/t/005_timeouts.pl
@@ -0,0 +1,104 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Test timeouts that will FATAL-out.
+# This test relies on an injection points to await timeout ocurance.
+# Relying on sleep prooved to be unstable on buildfarm.
+# It's difficult to rely on NOTICE injection point, because FATALed
+# backend can look differently under different circumstances.
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('master');
+$node->init();
+$node->start;
+
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('TransactionTimeout', 'wait');");
+
+my $psql_session =
+  $node->background_psql('postgres', on_error_stop => 0);
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET transaction_timeout to '1ms';
+   BEGIN;
+   $$
+));
+
+# Wait until the backend is in the timeout.
+ok( $node->poll_query_until(
+		'postgres',
+		qq[SELECT count(*) FROM pg_stat_activity
+           WHERE wait_event = 'TransactionTimeout' ;],
+		'1'),
+	'backend is waiting in transaction timeout'
+) or die "Timed out while waiting for transaction timeout";
+
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('TransactionTimeout');");
+$psql_session->quit;
+
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('IdleInTransactionSessionTimeout', 'wait');");
+
+$psql_session =
+  $node->background_psql('postgres', on_error_stop => 0);
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET idle_in_transaction_session_timeout to '10ms';
+   BEGIN;
+));
+
+# Wait until the backend is in the timeout.
+ok( $node->poll_query_until(
+		'postgres',
+		qq[SELECT count(*) FROM pg_stat_activity
+           WHERE wait_event = 'IdleInTransactionSessionTimeout' ;],
+		'1'),
+	'backend is waiting in idle in transaction session timeout'
+) or die "Timed out while waiting for idleness timeout";
+
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('IdleInTransactionSessionTimeout');");
+$psql_session->quit;
+
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('IdleSessionTimeout', 'wait');");
+
+$psql_session =
+  $node->background_psql('postgres', on_error_stop => 0);
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET idle_session_timeout to '10ms';
+));
+
+# Wait until the backend is in the timeout.
+ok( $node->poll_query_until(
+		'postgres',
+		qq[SELECT count(*) FROM pg_stat_activity
+           WHERE wait_event = 'IdleSessionTimeout' ;],
+		'1'),
+	'backend is waiting in idle session timeout'
+) or die "Timed out while waiting for idleness timeout";
+
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('IdleSessionTimeout');");
+$psql_session->quit;
+
+
+done_testing();
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] 0003-Add-regression-test-for-restart-points-during-promot.patch (9.2K, ../../[email protected]/4-0003-Add-regression-test-for-restart-points-during-promot.patch)
  download | inline diff:
From b13b2fe6a87ad348a5184096d244736d34ec556c Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email protected]>
Date: Wed, 21 Feb 2024 16:37:17 +0900
Subject: [PATCH 3/4] Add regression test for restart points during promotion

This test fails when 7863ee4def65 is reverted, checking that a node is
able to properly restart following a crash when a restart point was
finishing across a promotion.

This is an old bug that had no coverage, and injection points make that
cheap to achieve.
---
 src/backend/access/transam/xlog.c             |   7 +
 src/test/recovery/Makefile                    |   7 +-
 src/test/recovery/meson.build                 |   4 +
 .../t/041_invalid_checkpoint_after_promote.pl | 176 ++++++++++++++++++
 4 files changed, 193 insertions(+), 1 deletion(-)
 create mode 100644 src/test/recovery/t/041_invalid_checkpoint_after_promote.pl

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 50c347a679..50b045ff08 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -100,6 +100,7 @@
 #include "storage/sync.h"
 #include "utils/guc_hooks.h"
 #include "utils/guc_tables.h"
+#include "utils/injection_point.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
 #include "utils/relmapper.h"
@@ -7536,6 +7537,12 @@ CreateRestartPoint(int flags)
 
 	CheckPointGuts(lastCheckPoint.redo, flags);
 
+	/*
+	 * This location needs to be after CheckPointGuts() to ensure that some
+	 * work has already happened during this checkpoint.
+	 */
+	INJECTION_POINT("CreateRestartPoint");
+
 	/*
 	 * Remember the prior checkpoint's redo ptr for
 	 * UpdateCheckPointDistanceEstimate()
diff --git a/src/test/recovery/Makefile b/src/test/recovery/Makefile
index 17ee353735..f57baba5e8 100644
--- a/src/test/recovery/Makefile
+++ b/src/test/recovery/Makefile
@@ -9,12 +9,17 @@
 #
 #-------------------------------------------------------------------------
 
-EXTRA_INSTALL=contrib/pg_prewarm contrib/pg_stat_statements contrib/test_decoding
+EXTRA_INSTALL=contrib/pg_prewarm \
+	contrib/pg_stat_statements \
+	contrib/test_decoding \
+	src/test/modules/injection_points
 
 subdir = src/test/recovery
 top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
 
+export enable_injection_points enable_injection_points
+
 # required for 017_shm.pl and 027_stream_regress.pl
 REGRESS_SHLIB=$(abs_top_builddir)/src/test/regress/regress$(DLSUFFIX)
 export REGRESS_SHLIB
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index bf087ac2a9..e4e0e2b4cc 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -6,6 +6,9 @@ tests += {
   'bd': meson.current_build_dir(),
   'tap': {
     'test_kwargs': {'priority': 40}, # recovery tests are slow, start early
+    'env': {
+       'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+    },
     'tests': [
       't/001_stream_rep.pl',
       't/002_archiving.pl',
@@ -46,6 +49,7 @@ tests += {
       't/038_save_logical_slots_shutdown.pl',
       't/039_end_of_wal.pl',
       't/040_standby_failover_slots_sync.pl',
+      't/041_invalid_checkpoint_after_promote.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/041_invalid_checkpoint_after_promote.pl b/src/test/recovery/t/041_invalid_checkpoint_after_promote.pl
new file mode 100644
index 0000000000..e91f360d12
--- /dev/null
+++ b/src/test/recovery/t/041_invalid_checkpoint_after_promote.pl
@@ -0,0 +1,176 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Time::HiRes qw(usleep);
+use Test::More;
+
+##################################################
+# Test race condition when a restart point is running during a promotion,
+# checking that WAL segments are correctly removed in the restart point
+# while the promotion finishes.
+#
+# This test relies on an injection point that causes the checkpointer to
+# wait in the middle of a restart point on a standby.  The checkpointer
+# is awaken to finish its restart point only once the promotion of the
+# standby is completed, and the node should be able to restart properly.
+##################################################
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# Initialize primary node
+my $node_primary = PostgreSQL::Test::Cluster->new('master');
+$node_primary->init(allows_streaming => 1);
+$node_primary->append_conf(
+	'postgresql.conf', q[
+checkpoint_timeout = 30s
+log_checkpoints = on
+restart_after_crash = on
+]);
+$node_primary->start;
+
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+# Setup a standby
+my $node_standby = PostgreSQL::Test::Cluster->new('standby1');
+$node_standby->init_from_backup($node_primary, $backup_name,
+	has_streaming => 1);
+$node_standby->start;
+
+# Dummy table for the upcoming tests.
+$node_primary->safe_psql('postgres', 'checkpoint');
+$node_primary->safe_psql('postgres', 'CREATE TABLE prim_tab (a int);');
+
+# Register an injection point on the standby so as the follow-up
+# restart point will wait on it.
+$node_primary->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+# Wait until the extension has been created on the standby
+$node_primary->wait_for_replay_catchup($node_standby);
+
+# Note that from this point the checkpointer will wait in the middle of
+# a restart point on the standby.
+$node_standby->safe_psql('postgres',
+	"SELECT injection_points_attach('CreateRestartPoint', 'wait');");
+
+# Execute a restart point on the standby, that we will now be waiting on.
+# This needs to be in the background.
+my $logstart = -s $node_standby->logfile;
+my $psql_session =
+  $node_standby->background_psql('postgres', on_error_stop => 0);
+$psql_session->query_until(
+	qr/starting_checkpoint/, q(
+   \echo starting_checkpoint
+   CHECKPOINT;
+));
+
+# Switch one WAL segment to make the previous restart point remove the
+# segment once the restart point completes.
+$node_primary->safe_psql('postgres', 'INSERT INTO prim_tab VALUES (1);');
+$node_primary->safe_psql('postgres', 'SELECT pg_switch_wal();');
+$node_primary->wait_for_replay_catchup($node_standby);
+
+# Wait until the checkpointer is in the middle of the restart point
+# processing, relying on the custom wait event generated in the
+# wait callback used in the injection point previously attached.
+ok( $node_standby->poll_query_until(
+		'postgres',
+		qq[SELECT count(*) FROM pg_stat_activity
+           WHERE backend_type = 'checkpointer' AND wait_event = 'CreateRestartPoint' ;],
+		'1'),
+	'checkpointer is waiting in restart point'
+) or die "Timed out while waiting for checkpointer to run restart point";
+
+# Check the logs that the restart point has started on standby.  This is
+# optional, but let's be sure.
+my $log = slurp_file($node_standby->logfile, $logstart);
+my $checkpoint_start = 0;
+if ($log =~ m/restartpoint starting: immediate wait/)
+{
+	$checkpoint_start = 1;
+}
+is($checkpoint_start, 1, 'restartpoint has started');
+
+# Trigger promotion during the restart point.
+$node_primary->stop;
+$node_standby->promote;
+
+# Update the start position before waking up the checkpointer!
+$logstart = -s $node_standby->logfile;
+
+# Now wake up the checkpointer.
+$node_standby->safe_psql('postgres',
+	"SELECT injection_points_wakeup('CreateRestartPoint');");
+
+# Wait until the previous restart point completes on the newly-promoted
+# standby, checking the logs for that.
+my $checkpoint_complete = 0;
+foreach my $i (0 .. 10 * $PostgreSQL::Test::Utils::timeout_default)
+{
+	my $log = slurp_file($node_standby->logfile, $logstart);
+	if ($log =~ m/restartpoint complete/)
+	{
+		$checkpoint_complete = 1;
+		last;
+	}
+	usleep(100_000);
+}
+is($checkpoint_complete, 1, 'restart point has completed');
+
+# Kill with SIGKILL, forcing all the backends to restart.
+my $psql_timeout = IPC::Run::timer(3600);
+my ($killme_stdin, $killme_stdout, $killme_stderr) = ('', '', '');
+my $killme = IPC::Run::start(
+	[
+		'psql', '-XAtq', '-v', 'ON_ERROR_STOP=1', '-f', '-', '-d',
+		$node_standby->connstr('postgres')
+	],
+	'<',
+	\$killme_stdin,
+	'>',
+	\$killme_stdout,
+	'2>',
+	\$killme_stderr,
+	$psql_timeout);
+$killme_stdin .= q[
+SELECT pg_backend_pid();
+];
+$killme->pump until $killme_stdout =~ /[[:digit:]]+[\r\n]$/;
+my $pid = $killme_stdout;
+chomp($pid);
+$killme_stdout = '';
+$killme_stderr = '';
+
+my $ret = PostgreSQL::Test::Utils::system_log('pg_ctl', 'kill', 'KILL', $pid);
+is($ret, 0, 'killed process with KILL');
+
+# Wait until the server restarts, finish consuming output.
+$killme_stdin .= q[
+SELECT 1;
+];
+ok( pump_until(
+		$killme,
+		$psql_timeout,
+		\$killme_stderr,
+		qr/server closed the connection unexpectedly|connection to server was lost|could not send data to server/m
+	),
+	"psql query died successfully after SIGKILL");
+$killme->finish;
+
+# Wait till server finishes restarting
+$node_standby->poll_query_until('postgres', undef, '');
+
+# After recovery, the server should be able to start.
+my $stdout;
+my $stderr;
+($ret, $stdout, $stderr) = $node_standby->psql('postgres', 'select 1');
+is($ret, 0, "psql connect success");
+is($stdout, 1, "psql select 1");
+
+done_testing();
-- 
2.37.1 (Apple Git-137.1)



  [application/octet-stream] 0002-injection_points-Add-routines-to-wait-and-wake-proce.patch (7.1K, ../../[email protected]/5-0002-injection_points-Add-routines-to-wait-and-wake-proce.patch)
  download | inline diff:
From 6d3e4b4d77dc7dfe6cd3b939701a4ae790899c4e Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email protected]>
Date: Wed, 21 Feb 2024 16:36:25 +0900
Subject: [PATCH 2/4] injection_points: Add routines to wait and wake processes

This commit is made of two parts:
- A new callback that can be attached to a process to make it wait on a
condition variable.  The condition checked is registered in shared
memory by the module injection_points.
- A new SQL function to update the shared state and broadcast the update
using a condition variable.

The shared state used by the module is registered using the DSM
registry, and is optional.
---
 .../injection_points--1.0.sql                 |  10 ++
 .../injection_points/injection_points.c       | 151 ++++++++++++++++++
 src/tools/pgindent/typedefs.list              |   1 +
 3 files changed, 162 insertions(+)

diff --git a/src/test/modules/injection_points/injection_points--1.0.sql b/src/test/modules/injection_points/injection_points--1.0.sql
index 5944c41716..eed0310cf6 100644
--- a/src/test/modules/injection_points/injection_points--1.0.sql
+++ b/src/test/modules/injection_points/injection_points--1.0.sql
@@ -24,6 +24,16 @@ RETURNS void
 AS 'MODULE_PATHNAME', 'injection_points_run'
 LANGUAGE C STRICT PARALLEL UNSAFE;
 
+--
+-- injection_points_wakeup()
+--
+-- Wakes a condition variable waited on in an injection point.
+--
+CREATE FUNCTION injection_points_wakeup(IN point_name TEXT)
+RETURNS void
+AS 'MODULE_PATHNAME', 'injection_points_wakeup'
+LANGUAGE C STRICT PARALLEL UNSAFE;
+
 --
 -- injection_points_detach()
 --
diff --git a/src/test/modules/injection_points/injection_points.c b/src/test/modules/injection_points/injection_points.c
index e843e6594f..052b20f9c8 100644
--- a/src/test/modules/injection_points/injection_points.c
+++ b/src/test/modules/injection_points/injection_points.c
@@ -18,17 +18,71 @@
 #include "postgres.h"
 
 #include "fmgr.h"
+#include "storage/condition_variable.h"
 #include "storage/lwlock.h"
 #include "storage/shmem.h"
+#include "storage/dsm_registry.h"
 #include "utils/builtins.h"
 #include "utils/injection_point.h"
 #include "utils/wait_event.h"
 
 PG_MODULE_MAGIC;
 
+/* Maximum number of wait usable in injection points at once */
+#define INJ_MAX_WAIT	32
+#define INJ_NAME_MAXLEN	64
+
+/* Shared state information for injection points. */
+typedef struct InjectionPointSharedState
+{
+	/* protects accesses to wait_counts */
+	slock_t		lock;
+
+	/* Counters advancing when injection_points_wakeup() is called */
+	int			wait_counts[INJ_MAX_WAIT];
+
+	/* Names of injection points attached to wait counters */
+	char		name[INJ_MAX_WAIT][INJ_NAME_MAXLEN];
+
+	/*
+	 * Condition variable used for waits and wakeups, checking upon the set of
+	 * wait_counts when waiting.
+	 */
+	ConditionVariable wait_point;
+} InjectionPointSharedState;
+
+/* Pointer to shared-memory state. */
+static InjectionPointSharedState *inj_state = NULL;
+
 extern PGDLLEXPORT void injection_error(const char *name);
 extern PGDLLEXPORT void injection_notice(const char *name);
+extern PGDLLEXPORT void injection_wait(const char *name);
+
+
+static void
+injection_point_init_state(void *ptr)
+{
+	InjectionPointSharedState *state = (InjectionPointSharedState *) ptr;
+
+	SpinLockInit(&state->lock);
+	memset(state->wait_counts, 0, sizeof(state->wait_counts));
+	memset(state->name, 0, sizeof(state->name));
+	ConditionVariableInit(&state->wait_point);
+}
+
+static void
+injection_init_shmem(void)
+{
+	bool		found;
 
+	if (inj_state != NULL)
+		return;
+
+	inj_state = GetNamedDSMSegment("injection_points",
+								   sizeof(InjectionPointSharedState),
+								   injection_point_init_state,
+								   &found);
+}
 
 /* Set of callbacks available to be attached to an injection point. */
 void
@@ -43,6 +97,65 @@ injection_notice(const char *name)
 	elog(NOTICE, "notice triggered for injection point %s", name);
 }
 
+/* Wait on a condition variable, awaken by injection_points_wakeup() */
+void
+injection_wait(const char *name)
+{
+	int			old_wait_counts = -1;
+	int			index = -1;
+	uint32		injection_wait_event = 0;
+
+	if (inj_state == NULL)
+		injection_init_shmem();
+
+	/*
+	 * This custom wait event name is not released, but we don't care much for
+	 * testing as this will be short-lived.
+	 */
+	injection_wait_event = WaitEventExtensionNew(name);
+
+	/*
+	 * Find a free slot to wait for, and register this injection point's name.
+	 */
+	SpinLockAcquire(&inj_state->lock);
+	for (int i = 0; i < INJ_MAX_WAIT; i++)
+	{
+		if (inj_state->name[i][0] == '\0')
+		{
+			index = i;
+			strlcpy(inj_state->name[i], name, INJ_NAME_MAXLEN);
+			old_wait_counts = inj_state->wait_counts[i];
+			break;
+		}
+	}
+	SpinLockRelease(&inj_state->lock);
+
+	if (index < 0)
+		elog(ERROR, "could not find free slot for wait of injection point %s ",
+			 name);
+
+	/* And sleep.. */
+	ConditionVariablePrepareToSleep(&inj_state->wait_point);
+	for (;;)
+	{
+		int			new_wait_counts;
+
+		SpinLockAcquire(&inj_state->lock);
+		new_wait_counts = inj_state->wait_counts[index];
+		SpinLockRelease(&inj_state->lock);
+
+		if (old_wait_counts != new_wait_counts)
+			break;
+		ConditionVariableSleep(&inj_state->wait_point, injection_wait_event);
+	}
+	ConditionVariableCancelSleep();
+
+	/* Remove us from the waiting list */
+	SpinLockAcquire(&inj_state->lock);
+	inj_state->name[index][0] = '\0';
+	SpinLockRelease(&inj_state->lock);
+}
+
 /*
  * SQL function for creating an injection point.
  */
@@ -58,6 +171,8 @@ injection_points_attach(PG_FUNCTION_ARGS)
 		function = "injection_error";
 	else if (strcmp(action, "notice") == 0)
 		function = "injection_notice";
+	else if (strcmp(action, "wait") == 0)
+		function = "injection_wait";
 	else
 		elog(ERROR, "incorrect action \"%s\" for injection point creation", action);
 
@@ -80,6 +195,42 @@ injection_points_run(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+/*
+ * SQL function for waking a condition variable.
+ */
+PG_FUNCTION_INFO_V1(injection_points_wakeup);
+Datum
+injection_points_wakeup(PG_FUNCTION_ARGS)
+{
+	char	   *name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+	int			index = -1;
+
+	if (inj_state == NULL)
+		injection_init_shmem();
+
+	/* First bump the wait counter for the injection point to wake */
+	SpinLockAcquire(&inj_state->lock);
+	for (int i = 0; i < INJ_MAX_WAIT; i++)
+	{
+		if (strcmp(name, inj_state->name[i]) == 0)
+		{
+			index = i;
+			break;
+		}
+	}
+	if (index < 0)
+	{
+		SpinLockRelease(&inj_state->lock);
+		elog(ERROR, "could not find injection point %s to wake", name);
+	}
+	inj_state->wait_counts[index]++;
+	SpinLockRelease(&inj_state->lock);
+
+	/* And broadcast the change for the waiters */
+	ConditionVariableBroadcast(&inj_state->wait_point);
+	PG_RETURN_VOID();
+}
+
 /*
  * SQL function for dropping an injection point.
  */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index d808aad8b0..d7eca00502 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1208,6 +1208,7 @@ InitializeDSMForeignScan_function
 InitializeWorkerForeignScan_function
 InjectionPointCacheEntry
 InjectionPointEntry
+InjectionPointSharedState
 InlineCodeBlock
 InsertStmt
 Instrumentation
-- 
2.37.1 (Apple Git-137.1)



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-02-25 18:50  Alexander Korotkov <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Alexander Korotkov @ 2024-02-25 18:50 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: Japin Li <[email protected]>; Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrew Borodin <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; pgsql-hackers mailing list <[email protected]>; Andres Freund <[email protected]>

Hi, Andrey!

On Thu, Feb 22, 2024 at 7:23 PM Andrey M. Borodin <[email protected]> wrote:
> > On 19 Feb 2024, at 15:17, Japin Li <[email protected]> wrote:
> >
> >
> > +1
>
> PFA patch set of 4 patches:
> 1. remove all potential flaky tests. BTW recently we had a bingo when 3 of them failed together [0]
> 2-3. waiting injection points patchset by Michael Paquier, intact v2 from nearby thread.
> 4. prototype of simple TAP tests for timeouts.
>
> I did not add a test for statement_timeout, because it still have good coverage in isolation tests. But added test for idle_sessoin_timeout.
> Maybe these tests could be implemented with NOTICE injection points (not requiring steps 2-3), but I'm afraid that they might be flaky too: FATALed connection might not send information necesary for test success (we will see something like "PQconsumeInput failed: server closed the connection unexpectedly" as in [1]).

Thank you for the patches.  I've pushed the 0001 patch to avoid
further failures on buildfarm.  Let 0004 wait till injections points
by Mechael are committed.

------
Regards,
Alexander Korotkov






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-03-06 08:22  Andrey M. Borodin <[email protected]>
  parent: Alexander Korotkov <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2024-03-06 08:22 UTC (permalink / raw)
  To: Alexander Korotkov <[email protected]>; +Cc: Japin Li <[email protected]>; Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrew Borodin <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; pgsql-hackers mailing list <[email protected]>; Andres Freund <[email protected]>



> On 25 Feb 2024, at 21:50, Alexander Korotkov <[email protected]> wrote:
> 
> Thank you for the patches.  I've pushed the 0001 patch to avoid
> further failures on buildfarm.  Let 0004 wait till injections points
> by Mechael are committed.

Thanks!

All prerequisites are committed. I propose something in a line with this patch.


Best regards, Andrey Borodin.


Attachments:

  [application/octet-stream] v2-0001-Add-timeouts-TAP-tests.patch (6.9K, ../../[email protected]/2-v2-0001-Add-timeouts-TAP-tests.patch)
  download | inline diff:
From dcc076b00be7647c5f195845bf0c4917d24b5045 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Wed, 21 Feb 2024 19:34:26 +0300
Subject: [PATCH v2] Add timeouts TAP tests

These tests verify that transaction_timeout, idle_session_timeout
and idle_in_transaction_session_timeout work as expected.
To do so we add injection points in before throwing a FATAL
and test that these injection points are reached.

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 src/backend/tcop/postgres.c                  |  10 ++
 src/test/modules/test_misc/Makefile          |   4 +
 src/test/modules/test_misc/meson.build       |   4 +
 src/test/modules/test_misc/t/005_timeouts.pl | 115 +++++++++++++++++++
 4 files changed, 133 insertions(+)
 create mode 100644 src/test/modules/test_misc/t/005_timeouts.pl

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index aec1b19442..be5cb2cf2c 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -72,6 +72,7 @@
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
 #include "utils/guc_hooks.h"
+#include "utils/injection_point.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
@@ -3411,9 +3412,12 @@ ProcessInterrupts(void)
 		 * interrupt.
 		 */
 		if (IdleInTransactionSessionTimeout > 0)
+		{
+			INJECTION_POINT("idle-in-transaction-session-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT),
 					 errmsg("terminating connection due to idle-in-transaction timeout")));
+		}
 		else
 			IdleInTransactionSessionTimeoutPending = false;
 	}
@@ -3422,9 +3426,12 @@ ProcessInterrupts(void)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
 		if (TransactionTimeout > 0)
+		{
+			INJECTION_POINT("transaction-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
 					 errmsg("terminating connection due to transaction timeout")));
+		}
 		else
 			TransactionTimeoutPending = false;
 	}
@@ -3433,9 +3440,12 @@ ProcessInterrupts(void)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
 		if (IdleSessionTimeout > 0)
+		{
+			INJECTION_POINT("idle-session-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_IDLE_SESSION_TIMEOUT),
 					 errmsg("terminating connection due to idle-session timeout")));
+		}
 		else
 			IdleSessionTimeoutPending = false;
 	}
diff --git a/src/test/modules/test_misc/Makefile b/src/test/modules/test_misc/Makefile
index 39c6c2014a..a958d156f4 100644
--- a/src/test/modules/test_misc/Makefile
+++ b/src/test/modules/test_misc/Makefile
@@ -2,6 +2,10 @@
 
 TAP_TESTS = 1
 
+EXTRA_INSTALL=src/test/modules/injection_points
+
+export enable_injection_points enable_injection_points
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 964d95db26..df2913e893 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -5,11 +5,15 @@ tests += {
   'sd': meson.current_source_dir(),
   'bd': meson.current_build_dir(),
   'tap': {
+    'env': {
+       'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+    },
     'tests': [
       't/001_constraint_validation.pl',
       't/002_tablespace.pl',
       't/003_check_guc.pl',
       't/004_io_direct.pl',
+      't/005_timeouts.pl'
     ],
   },
 }
diff --git a/src/test/modules/test_misc/t/005_timeouts.pl b/src/test/modules/test_misc/t/005_timeouts.pl
new file mode 100644
index 0000000000..fa76714816
--- /dev/null
+++ b/src/test/modules/test_misc/t/005_timeouts.pl
@@ -0,0 +1,115 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Test timeouts that will FATAL-out.
+# This test relies on an injection points to await timeout ocurance.
+# Relying on sleep prooved to be unstable on buildfarm.
+# It's difficult to rely on NOTICE injection point, because FATALed
+# backend can look differently under different circumstances.
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# Node initialization
+my $node = PostgreSQL::Test::Cluster->new('master');
+$node->init();
+$node->start;
+
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('transaction-timeout', 'wait');");
+
+
+#
+# 1. Test of transaction timeout
+#
+
+my $psql_session =
+  $node->background_psql('postgres');
+
+# Following query will generate a stream of SELECT 1 queries. This is done to
+# excersice transaction timeout in presence of short queries.
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET transaction_timeout to '10ms';
+   BEGIN;
+   SELECT 1 \watch 0.001
+   \q
+));
+
+# Wait until the backend is in the timeout.
+# In case if anything get broken this waiting will error-out
+$node->wait_for_event('client backend','transaction-timeout');
+
+# Remove injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('transaction-timeout');");
+
+# If we send \q with $psql_session->quit it can get to pump already closed.
+# So \q is in initial script, here we only finish IPC::Run.
+$psql_session->{run}->finish;
+
+
+#
+# 2. Test of idle in transaction timeout
+#
+
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('idle-in-transaction-session-timeout', 'wait');");
+
+# We begin a transaction and the hand on the line
+$psql_session =
+  $node->background_psql('postgres');
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET idle_in_transaction_session_timeout to '10ms';
+   BEGIN;
+));
+
+# Wait until the backend is in the timeout.
+$node->wait_for_event('client backend','idle-in-transaction-session-timeout');
+
+# Remove injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('idle-in-transaction-session-timeout');");
+ok($psql_session->quit);
+
+
+#
+# 3. Test of idle session timeout
+#
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('idle-session-timeout', 'wait');");
+
+# We just initialize GUC and wait. No transaction required.
+$psql_session =
+  $node->background_psql('postgres');
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET idle_session_timeout to '10ms';
+));
+
+# Wait until the backend is in the timeout.
+$node->wait_for_event('client backend','idle-session-timeout');
+
+# Remove injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('idle-session-timeout');");
+ok($psql_session->quit);
+
+
+# Tests above will hang if injection points are not reached
+ok(1);
+
+done_testing();
-- 
2.37.1 (Apple Git-137.1)



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-03-06 19:55  Alexander Korotkov <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Alexander Korotkov @ 2024-03-06 19:55 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: Japin Li <[email protected]>; Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrew Borodin <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; pgsql-hackers mailing list <[email protected]>; Andres Freund <[email protected]>

On Wed, Mar 6, 2024 at 10:22 AM Andrey M. Borodin <[email protected]> wrote:
> > On 25 Feb 2024, at 21:50, Alexander Korotkov <[email protected]> wrote:
> >
> > Thank you for the patches.  I've pushed the 0001 patch to avoid
> > further failures on buildfarm.  Let 0004 wait till injections points
> > by Mechael are committed.
>
> Thanks!
>
> All prerequisites are committed. I propose something in a line with this patch.

Thank you.  I took a look at the patch.  Should we also check the
relevant message after the timeout is fired?  We could check it in
psql stderr or log for that.

------
Regards,
Alexander Korotkov






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-03-11 10:52  Andrey M. Borodin <[email protected]>
  parent: Alexander Korotkov <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2024-03-11 10:52 UTC (permalink / raw)
  To: Alexander Korotkov <[email protected]>; +Cc: Japin Li <[email protected]>; Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrew Borodin <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; pgsql-hackers mailing list <[email protected]>; Andres Freund <[email protected]>



> On 7 Mar 2024, at 00:55, Alexander Korotkov <[email protected]> wrote:
> 
> On Wed, Mar 6, 2024 at 10:22 AM Andrey M. Borodin <[email protected]> wrote:
>>> On 25 Feb 2024, at 21:50, Alexander Korotkov <[email protected]> wrote:
>>> 
>>> Thank you for the patches.  I've pushed the 0001 patch to avoid
>>> further failures on buildfarm.  Let 0004 wait till injections points
>>> by Mechael are committed.
>> 
>> Thanks!
>> 
>> All prerequisites are committed. I propose something in a line with this patch.
> 
> Thank you.  I took a look at the patch.  Should we also check the
> relevant message after the timeout is fired?  We could check it in
> psql stderr or log for that.

PFA version which checks log output.
But I could not come up with a proper use of BackgroundPsql->query_until() to check outputs. And there are multiple possible errors.

We can copy test from src/bin/psql/t/001_basic.pl:

# test behavior and output on server crash
my ($ret, $out, $err) = $node->psql('postgres',
"SELECT 'before' AS running;\n"
. "SELECT pg_terminate_backend(pg_backend_pid());\n"
. "SELECT 'AFTER' AS not_running;\n");

is($ret, 2, 'server crash: psql exit code');
like($out, qr/before/, 'server crash: output before crash');
ok($out !~ qr/AFTER/, 'server crash: no output after crash');
is( $err,
'psql:<stdin>:2: FATAL: terminating connection due to administrator command
psql:<stdin>:2: server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
psql:<stdin>:2: error: connection to server was lost',
'server crash: error message’);

But I do not see much value in this.
What do you think?


Best regards, Andrey Borodin.



Attachments:

  [application/octet-stream] v3-0001-Add-timeouts-TAP-tests.patch (8.1K, ../../[email protected]/2-v3-0001-Add-timeouts-TAP-tests.patch)
  download | inline diff:
From d16faf3f011456f47044c243c83fb268cac8ee27 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Wed, 21 Feb 2024 19:34:26 +0300
Subject: [PATCH v3] Add timeouts TAP tests

These tests verify that transaction_timeout, idle_session_timeout
and idle_in_transaction_session_timeout work as expected.
To do so we add injection points in before throwing a FATAL
and test that these injection points are reached.

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 src/backend/tcop/postgres.c                  |  10 ++
 src/test/modules/test_misc/Makefile          |   4 +
 src/test/modules/test_misc/meson.build       |   4 +
 src/test/modules/test_misc/t/005_timeouts.pl | 149 +++++++++++++++++++
 4 files changed, 167 insertions(+)
 create mode 100644 src/test/modules/test_misc/t/005_timeouts.pl

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index aec1b194424..be5cb2cf2c2 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -72,6 +72,7 @@
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
 #include "utils/guc_hooks.h"
+#include "utils/injection_point.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
@@ -3411,9 +3412,12 @@ ProcessInterrupts(void)
 		 * interrupt.
 		 */
 		if (IdleInTransactionSessionTimeout > 0)
+		{
+			INJECTION_POINT("idle-in-transaction-session-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT),
 					 errmsg("terminating connection due to idle-in-transaction timeout")));
+		}
 		else
 			IdleInTransactionSessionTimeoutPending = false;
 	}
@@ -3422,9 +3426,12 @@ ProcessInterrupts(void)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
 		if (TransactionTimeout > 0)
+		{
+			INJECTION_POINT("transaction-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
 					 errmsg("terminating connection due to transaction timeout")));
+		}
 		else
 			TransactionTimeoutPending = false;
 	}
@@ -3433,9 +3440,12 @@ ProcessInterrupts(void)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
 		if (IdleSessionTimeout > 0)
+		{
+			INJECTION_POINT("idle-session-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_IDLE_SESSION_TIMEOUT),
 					 errmsg("terminating connection due to idle-session timeout")));
+		}
 		else
 			IdleSessionTimeoutPending = false;
 	}
diff --git a/src/test/modules/test_misc/Makefile b/src/test/modules/test_misc/Makefile
index 39c6c2014a0..a958d156f47 100644
--- a/src/test/modules/test_misc/Makefile
+++ b/src/test/modules/test_misc/Makefile
@@ -2,6 +2,10 @@
 
 TAP_TESTS = 1
 
+EXTRA_INSTALL=src/test/modules/injection_points
+
+export enable_injection_points enable_injection_points
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 964d95db263..df2913e8938 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -5,11 +5,15 @@ tests += {
   'sd': meson.current_source_dir(),
   'bd': meson.current_build_dir(),
   'tap': {
+    'env': {
+       'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+    },
     'tests': [
       't/001_constraint_validation.pl',
       't/002_tablespace.pl',
       't/003_check_guc.pl',
       't/004_io_direct.pl',
+      't/005_timeouts.pl'
     ],
   },
 }
diff --git a/src/test/modules/test_misc/t/005_timeouts.pl b/src/test/modules/test_misc/t/005_timeouts.pl
new file mode 100644
index 00000000000..fb8e7a07df3
--- /dev/null
+++ b/src/test/modules/test_misc/t/005_timeouts.pl
@@ -0,0 +1,149 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Time::HiRes qw(usleep);
+use Test::More;
+
+# Test timeouts that will FATAL-out.
+# This test relies on an injection points to await timeout ocurance.
+# Relying on sleep prooved to be unstable on buildfarm.
+# It's difficult to rely on NOTICE injection point, because FATALed
+# backend can look differently under different circumstances.
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# Node initialization
+my $node = PostgreSQL::Test::Cluster->new('master');
+$node->init();
+$node->start;
+
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('transaction-timeout', 'wait');");
+
+# Update the start position to check logs later
+my $logstart = -s $node->logfile;
+
+
+#
+# 1. Test of transaction timeout
+#
+
+my $psql_session =
+  $node->background_psql('postgres');
+
+# Following query will generate a stream of SELECT 1 queries. This is done to
+# excersice transaction timeout in presence of short queries.
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET transaction_timeout to '10ms';
+   BEGIN;
+   SELECT 1 \watch 0.001
+   \q
+));
+
+# Wait until the backend is in the timeout.
+# In case if anything get broken this waiting will error-out
+$node->wait_for_event('client backend','transaction-timeout');
+
+# Remove injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('transaction-timeout');");
+
+# If we send \q with $psql_session->quit it can get to pump already closed.
+# So \q is in initial script, here we only finish IPC::Run.
+$psql_session->{run}->finish;
+
+
+#
+# 2. Test of idle in transaction timeout
+#
+
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('idle-in-transaction-session-timeout', 'wait');");
+
+# We begin a transaction and the hand on the line
+$psql_session =
+  $node->background_psql('postgres');
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET idle_in_transaction_session_timeout to '10ms';
+   BEGIN;
+));
+
+# Wait until the backend is in the timeout.
+$node->wait_for_event('client backend','idle-in-transaction-session-timeout');
+
+# Remove injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('idle-in-transaction-session-timeout');");
+ok($psql_session->quit);
+
+
+#
+# 3. Test of idle session timeout
+#
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('idle-session-timeout', 'wait');");
+
+# We just initialize GUC and wait. No transaction required.
+$psql_session =
+  $node->background_psql('postgres');
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET idle_session_timeout to '10ms';
+));
+
+# Wait until the backend is in the timeout.
+$node->wait_for_event('client backend','idle-session-timeout');
+
+# Remove injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('idle-session-timeout');");
+ok($psql_session->quit);
+
+# Check that every timeout was logged
+my $transaction_timeout_logged = 0;
+my $idle_in_transaction_session_timeout_logged = 0;
+my $idle_session_timeout_logged = 0;
+foreach my $i (0 .. 10 * $PostgreSQL::Test::Utils::timeout_default)
+{
+	if ((!$transaction_timeout_logged) &&
+      $node->log_contains('terminating connection due to transaction timeout', $logstart))
+	{
+		$transaction_timeout_logged = 1;
+	}
+	if ((!$idle_in_transaction_session_timeout_logged) &&
+      $node->log_contains('terminating connection due to idle-in-transaction timeout', $logstart))
+	{
+		$idle_in_transaction_session_timeout_logged = 1;
+	}
+	if ((!$idle_session_timeout_logged) &&
+      $node->log_contains('terminating connection due to idle-session timeout', $logstart))
+	{
+		$idle_session_timeout_logged = 1;
+	}
+
+  if ($transaction_timeout_logged && $idle_in_transaction_session_timeout_logged &&
+      $idle_session_timeout_logged)
+  {
+		last;
+  }
+
+	usleep(100_000);
+}
+ok($transaction_timeout_logged, 'transaction timeout was logged');
+ok($idle_in_transaction_session_timeout_logged, 'transaction timeout was logged');
+ok($idle_session_timeout_logged, 'transaction timeout was logged');
+
+done_testing();
-- 
2.42.0



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-03-11 11:18  Alexander Korotkov <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Alexander Korotkov @ 2024-03-11 11:18 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: Japin Li <[email protected]>; Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrew Borodin <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; pgsql-hackers mailing list <[email protected]>; Andres Freund <[email protected]>

On Mon, Mar 11, 2024 at 12:53 PM Andrey M. Borodin <[email protected]> wrote:
> > On 7 Mar 2024, at 00:55, Alexander Korotkov <[email protected]> wrote:
> >
> > On Wed, Mar 6, 2024 at 10:22 AM Andrey M. Borodin <[email protected]> wrote:
> >>> On 25 Feb 2024, at 21:50, Alexander Korotkov <[email protected]> wrote:
> >>>
> >>> Thank you for the patches.  I've pushed the 0001 patch to avoid
> >>> further failures on buildfarm.  Let 0004 wait till injections points
> >>> by Mechael are committed.
> >>
> >> Thanks!
> >>
> >> All prerequisites are committed. I propose something in a line with this patch.
> >
> > Thank you.  I took a look at the patch.  Should we also check the
> > relevant message after the timeout is fired?  We could check it in
> > psql stderr or log for that.
>
> PFA version which checks log output.
> But I could not come up with a proper use of BackgroundPsql->query_until() to check outputs. And there are multiple possible errors.
>
> We can copy test from src/bin/psql/t/001_basic.pl:
>
> # test behavior and output on server crash
> my ($ret, $out, $err) = $node->psql('postgres',
> "SELECT 'before' AS running;\n"
> . "SELECT pg_terminate_backend(pg_backend_pid());\n"
> . "SELECT 'AFTER' AS not_running;\n");
>
> is($ret, 2, 'server crash: psql exit code');
> like($out, qr/before/, 'server crash: output before crash');
> ok($out !~ qr/AFTER/, 'server crash: no output after crash');
> is( $err,
> 'psql:<stdin>:2: FATAL: terminating connection due to administrator command
> psql:<stdin>:2: server closed the connection unexpectedly
> This probably means the server terminated abnormally
> before or while processing the request.
> psql:<stdin>:2: error: connection to server was lost',
> 'server crash: error message’);
>
> But I do not see much value in this.
> What do you think?

I think if checking psql stderr is problematic, checking just logs is
fine.  Could we wait for the relevant log messages one by one with
$node->wait_for_log() just like 040_standby_failover_slots_sync.pl do?

------
Regards,
Alexander Korotkov






^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-03-12 08:28  Andrey M. Borodin <[email protected]>
  parent: Alexander Korotkov <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2024-03-12 08:28 UTC (permalink / raw)
  To: Alexander Korotkov <[email protected]>; +Cc: Japin Li <[email protected]>; Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrew Borodin <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; pgsql-hackers mailing list <[email protected]>; Andres Freund <[email protected]>



> On 11 Mar 2024, at 16:18, Alexander Korotkov <[email protected]> wrote:
> 
> I think if checking psql stderr is problematic, checking just logs is
> fine.  Could we wait for the relevant log messages one by one with
> $node->wait_for_log() just like 040_standby_failover_slots_sync.pl do?

PFA version with $node->wait_for_log()

Best regards, Andrey Borodin.


Attachments:

  [application/octet-stream] v4-0001-Add-timeouts-TAP-tests.patch (7.1K, ../../[email protected]/2-v4-0001-Add-timeouts-TAP-tests.patch)
  download | inline diff:
From 06d0a39115da7ce2290a3cb64645b70a77b40686 Mon Sep 17 00:00:00 2001
From: "Andrey M. Borodin" <[email protected]>
Date: Wed, 21 Feb 2024 19:34:26 +0300
Subject: [PATCH v4] Add timeouts TAP tests

These tests verify that transaction_timeout, idle_session_timeout
and idle_in_transaction_session_timeout work as expected.
To do so we add injection points in before throwing a FATAL
and test that these injection points are reached.

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
---
 src/backend/tcop/postgres.c                  |  10 ++
 src/test/modules/test_misc/Makefile          |   4 +
 src/test/modules/test_misc/meson.build       |   4 +
 src/test/modules/test_misc/t/005_timeouts.pl | 118 +++++++++++++++++++
 4 files changed, 136 insertions(+)
 create mode 100644 src/test/modules/test_misc/t/005_timeouts.pl

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index aec1b19442..be5cb2cf2c 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -72,6 +72,7 @@
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
 #include "utils/guc_hooks.h"
+#include "utils/injection_point.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
@@ -3411,9 +3412,12 @@ ProcessInterrupts(void)
 		 * interrupt.
 		 */
 		if (IdleInTransactionSessionTimeout > 0)
+		{
+			INJECTION_POINT("idle-in-transaction-session-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT),
 					 errmsg("terminating connection due to idle-in-transaction timeout")));
+		}
 		else
 			IdleInTransactionSessionTimeoutPending = false;
 	}
@@ -3422,9 +3426,12 @@ ProcessInterrupts(void)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
 		if (TransactionTimeout > 0)
+		{
+			INJECTION_POINT("transaction-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
 					 errmsg("terminating connection due to transaction timeout")));
+		}
 		else
 			TransactionTimeoutPending = false;
 	}
@@ -3433,9 +3440,12 @@ ProcessInterrupts(void)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
 		if (IdleSessionTimeout > 0)
+		{
+			INJECTION_POINT("idle-session-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_IDLE_SESSION_TIMEOUT),
 					 errmsg("terminating connection due to idle-session timeout")));
+		}
 		else
 			IdleSessionTimeoutPending = false;
 	}
diff --git a/src/test/modules/test_misc/Makefile b/src/test/modules/test_misc/Makefile
index 39c6c2014a..a958d156f4 100644
--- a/src/test/modules/test_misc/Makefile
+++ b/src/test/modules/test_misc/Makefile
@@ -2,6 +2,10 @@
 
 TAP_TESTS = 1
 
+EXTRA_INSTALL=src/test/modules/injection_points
+
+export enable_injection_points enable_injection_points
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 964d95db26..df2913e893 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -5,11 +5,15 @@ tests += {
   'sd': meson.current_source_dir(),
   'bd': meson.current_build_dir(),
   'tap': {
+    'env': {
+       'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+    },
     'tests': [
       't/001_constraint_validation.pl',
       't/002_tablespace.pl',
       't/003_check_guc.pl',
       't/004_io_direct.pl',
+      't/005_timeouts.pl'
     ],
   },
 }
diff --git a/src/test/modules/test_misc/t/005_timeouts.pl b/src/test/modules/test_misc/t/005_timeouts.pl
new file mode 100644
index 0000000000..3046eca641
--- /dev/null
+++ b/src/test/modules/test_misc/t/005_timeouts.pl
@@ -0,0 +1,118 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Time::HiRes qw(usleep);
+use Test::More;
+
+# Test timeouts that will FATAL-out.
+# This test relies on an injection points to await timeout ocurance.
+# Relying on sleep prooved to be unstable on buildfarm.
+# It's difficult to rely on NOTICE injection point, because FATALed
+# backend can look differently under different circumstances.
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# Node initialization
+my $node = PostgreSQL::Test::Cluster->new('master');
+$node->init();
+$node->start;
+
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('transaction-timeout', 'wait');");
+
+
+#
+# 1. Test of transaction timeout
+#
+
+my $psql_session =
+  $node->background_psql('postgres');
+
+# Following query will generate a stream of SELECT 1 queries. This is done to
+# excersice transaction timeout in presence of short queries.
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET transaction_timeout to '10ms';
+   BEGIN;
+   SELECT 1 \watch 0.001
+   \q
+));
+
+# Wait until the backend is in the timeout.
+# In case if anything get broken this waiting will error-out
+$node->wait_for_event('client backend','transaction-timeout');
+
+# Remove injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('transaction-timeout');");
+
+# If we send \q with $psql_session->quit it can get to pump already closed.
+# So \q is in initial script, here we only finish IPC::Run.
+$psql_session->{run}->finish;
+
+
+#
+# 2. Test of idle in transaction timeout
+#
+
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('idle-in-transaction-session-timeout', 'wait');");
+
+# We begin a transaction and the hand on the line
+$psql_session =
+  $node->background_psql('postgres');
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET idle_in_transaction_session_timeout to '10ms';
+   BEGIN;
+));
+
+# Wait until the backend is in the timeout.
+$node->wait_for_event('client backend','idle-in-transaction-session-timeout');
+
+# Remove injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('idle-in-transaction-session-timeout');");
+ok($psql_session->quit);
+
+
+#
+# 3. Test of idle session timeout
+#
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('idle-session-timeout', 'wait');");
+
+# We just initialize GUC and wait. No transaction required.
+$psql_session =
+  $node->background_psql('postgres');
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET idle_session_timeout to '10ms';
+));
+
+# Wait until the backend is in the timeout.
+$node->wait_for_event('client backend','idle-session-timeout');
+
+# Remove injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('idle-session-timeout');");
+ok($psql_session->quit);
+
+
+# Check that every timeout was logged
+$node->wait_for_log('terminating connection due to transaction timeout');
+$node->wait_for_log('terminating connection due to idle-in-transaction timeout');
+$node->wait_for_log('terminating connection due to idle-session timeout');
+
+done_testing();
-- 
2.37.1 (Apple Git-137.1)



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-03-13 00:23  Alexander Korotkov <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Alexander Korotkov @ 2024-03-13 00:23 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: Japin Li <[email protected]>; Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrew Borodin <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; pgsql-hackers mailing list <[email protected]>; Andres Freund <[email protected]>

On Tue, Mar 12, 2024 at 10:28 AM Andrey M. Borodin <[email protected]> wrote:
> > On 11 Mar 2024, at 16:18, Alexander Korotkov <[email protected]> wrote:
> >
> > I think if checking psql stderr is problematic, checking just logs is
> > fine.  Could we wait for the relevant log messages one by one with
> > $node->wait_for_log() just like 040_standby_failover_slots_sync.pl do?
>
> PFA version with $node->wait_for_log()

I've slightly revised the patch.  I'm going to push it if no objections.

------
Regards,
Alexander Korotkov


Attachments:

  [application/octet-stream] v5-0001-Add-TAP-tests-for-timeouts.patch (7.4K, ../../CAPpHfdvo1Q7uu9RRCWxC2+OV1LuLthPFyUw6PKgzDtQr9brj4g@mail.gmail.com/2-v5-0001-Add-TAP-tests-for-timeouts.patch)
  download | inline diff:
From 8cd1f0bd5c92362d66f937973a3337bc73929e61 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email protected]>
Date: Wed, 13 Mar 2024 01:54:31 +0200
Subject: [PATCH v5] Add TAP tests for timeouts

This commit adds new tests to verify that transaction_timeout,
idle_session_timeout, and idle_in_transaction_session_timeout work as expected.
We introduce new injection points in before throwing a timeout FATAL error
and check these injection points are reached.

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
Author: Andrey Borodin
Reviewed-by: Alexander Korotkov
---
 src/backend/tcop/postgres.c                  |  10 ++
 src/test/modules/test_misc/Makefile          |   4 +
 src/test/modules/test_misc/meson.build       |   4 +
 src/test/modules/test_misc/t/005_timeouts.pl | 127 +++++++++++++++++++
 4 files changed, 145 insertions(+)
 create mode 100644 src/test/modules/test_misc/t/005_timeouts.pl

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 6b7903314ab..7ac623019bc 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -72,6 +72,7 @@
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
 #include "utils/guc_hooks.h"
+#include "utils/injection_point.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
@@ -3411,9 +3412,12 @@ ProcessInterrupts(void)
 		 * interrupt.
 		 */
 		if (IdleInTransactionSessionTimeout > 0)
+		{
+			INJECTION_POINT("idle-in-transaction-session-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT),
 					 errmsg("terminating connection due to idle-in-transaction timeout")));
+		}
 		else
 			IdleInTransactionSessionTimeoutPending = false;
 	}
@@ -3422,9 +3426,12 @@ ProcessInterrupts(void)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
 		if (TransactionTimeout > 0)
+		{
+			INJECTION_POINT("transaction-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
 					 errmsg("terminating connection due to transaction timeout")));
+		}
 		else
 			TransactionTimeoutPending = false;
 	}
@@ -3433,9 +3440,12 @@ ProcessInterrupts(void)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
 		if (IdleSessionTimeout > 0)
+		{
+			INJECTION_POINT("idle-session-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_IDLE_SESSION_TIMEOUT),
 					 errmsg("terminating connection due to idle-session timeout")));
+		}
 		else
 			IdleSessionTimeoutPending = false;
 	}
diff --git a/src/test/modules/test_misc/Makefile b/src/test/modules/test_misc/Makefile
index 39c6c2014a0..a958d156f47 100644
--- a/src/test/modules/test_misc/Makefile
+++ b/src/test/modules/test_misc/Makefile
@@ -2,6 +2,10 @@
 
 TAP_TESTS = 1
 
+EXTRA_INSTALL=src/test/modules/injection_points
+
+export enable_injection_points enable_injection_points
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 964d95db263..df2913e8938 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -5,11 +5,15 @@ tests += {
   'sd': meson.current_source_dir(),
   'bd': meson.current_build_dir(),
   'tap': {
+    'env': {
+       'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+    },
     'tests': [
       't/001_constraint_validation.pl',
       't/002_tablespace.pl',
       't/003_check_guc.pl',
       't/004_io_direct.pl',
+      't/005_timeouts.pl'
     ],
   },
 }
diff --git a/src/test/modules/test_misc/t/005_timeouts.pl b/src/test/modules/test_misc/t/005_timeouts.pl
new file mode 100644
index 00000000000..7cb37b843b5
--- /dev/null
+++ b/src/test/modules/test_misc/t/005_timeouts.pl
@@ -0,0 +1,127 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Time::HiRes qw(usleep);
+use Test::More;
+
+# Test timeouts that will cause FATAL errors.  This test relies on injection
+# points to await a timeout occurrence. Relying on sleep proved to be unstable
+# on buildfarm. It's difficult to rely on the NOTICE injection point because
+# the backend under FATAL error can behave differently.
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# Node initialization
+my $node = PostgreSQL::Test::Cluster->new('master');
+$node->init();
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+#
+# 1. Test of the transaction timeout
+#
+
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('transaction-timeout', 'wait');");
+
+my $psql_session = $node->background_psql('postgres');
+
+# The following query will generate a stream of SELECT 1 queries. This is done
+# so to exercise transaction timeout in the presence of short queries.
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET transaction_timeout to '10ms';
+   BEGIN;
+   SELECT 1 \watch 0.001
+   \q
+));
+
+# Wait until the backend is in the timeout injection point. Will get an error
+# here if anything goes wrong.
+$node->wait_for_event('client backend', 'transaction-timeout');
+
+my $log_offset = -s $node->logfile;
+
+# Remove the injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('transaction-timeout');");
+
+# Check that the timeout was logged.
+$node->wait_for_log('terminating connection due to transaction timeout');
+
+# If we send \q with $psql_session->quit it can get to pump already closed.
+# So \q is in initial script, here we only finish IPC::Run.
+$psql_session->{run}->finish;
+
+
+#
+# 2. Test of the idle in transaction timeout
+#
+
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('idle-in-transaction-session-timeout', 'wait');"
+);
+
+# We begin a transaction and the hand on the line
+$psql_session = $node->background_psql('postgres');
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET idle_in_transaction_session_timeout to '10ms';
+   BEGIN;
+));
+
+# Wait until the backend is in the timeout injection point.
+$node->wait_for_event('client backend',
+	'idle-in-transaction-session-timeout');
+
+$log_offset = -s $node->logfile;
+
+# Remove the injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('idle-in-transaction-session-timeout');");
+
+# Check that the timeout was logged.
+$node->wait_for_log(
+	'terminating connection due to idle-in-transaction timeout');
+
+ok($psql_session->quit);
+
+
+#
+# 3. Test of the idle session timeout
+#
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('idle-session-timeout', 'wait');");
+
+# We just initialize the GUC and wait. No transaction is required.
+$psql_session = $node->background_psql('postgres');
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET idle_session_timeout to '10ms';
+));
+
+# Wait until the backend is in the timeout injection point.
+$node->wait_for_event('client backend', 'idle-session-timeout');
+
+$log_offset = -s $node->logfile;
+
+# Remove the injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('idle-session-timeout');");
+
+# Check that the timeout was logged.
+$node->wait_for_log('terminating connection due to idle-session timeout');
+
+ok($psql_session->quit);
+
+done_testing();
-- 
2.39.3 (Apple Git-145)



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-03-13 05:56  Andrey M. Borodin <[email protected]>
  parent: Alexander Korotkov <[email protected]>
  0 siblings, 1 reply; 76+ messages in thread

From: Andrey M. Borodin @ 2024-03-13 05:56 UTC (permalink / raw)
  To: Alexander Korotkov <[email protected]>; +Cc: Japin Li <[email protected]>; Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrew Borodin <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; pgsql-hackers mailing list <[email protected]>; Andres Freund <[email protected]>



> On 13 Mar 2024, at 05:23, Alexander Korotkov <[email protected]> wrote:
> 
> On Tue, Mar 12, 2024 at 10:28 AM Andrey M. Borodin <[email protected]> wrote:
>>> On 11 Mar 2024, at 16:18, Alexander Korotkov <[email protected]> wrote:
>>> 
>>> I think if checking psql stderr is problematic, checking just logs is
>>> fine.  Could we wait for the relevant log messages one by one with
>>> $node->wait_for_log() just like 040_standby_failover_slots_sync.pl do?
>> 
>> PFA version with $node->wait_for_log()
> 
> I've slightly revised the patch.  I'm going to push it if no objections.

One small note: log_offset was updated, but was not used.

Thanks!


Best regards, Andrey Borodin.


Attachments:

  [application/octet-stream] v6-0001-Add-TAP-tests-for-timeouts.patch (7.5K, ../../[email protected]/2-v6-0001-Add-TAP-tests-for-timeouts.patch)
  download | inline diff:
From b87ef53cc24188ac633cb1863bd4db5d0c74c6c5 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email protected]>
Date: Wed, 13 Mar 2024 01:54:31 +0200
Subject: [PATCH v6] Add TAP tests for timeouts

This commit adds new tests to verify that transaction_timeout,
idle_session_timeout, and idle_in_transaction_session_timeout work as expected.
We introduce new injection points in before throwing a timeout FATAL error
and check these injection points are reached.

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
Author: Andrey Borodin
Reviewed-by: Alexander Korotkov
---
 src/backend/tcop/postgres.c                  |  10 ++
 src/test/modules/test_misc/Makefile          |   4 +
 src/test/modules/test_misc/meson.build       |   4 +
 src/test/modules/test_misc/t/005_timeouts.pl | 128 +++++++++++++++++++
 4 files changed, 146 insertions(+)
 create mode 100644 src/test/modules/test_misc/t/005_timeouts.pl

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 6b7903314ab..7ac623019bc 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -72,6 +72,7 @@
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
 #include "utils/guc_hooks.h"
+#include "utils/injection_point.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
@@ -3411,9 +3412,12 @@ ProcessInterrupts(void)
 		 * interrupt.
 		 */
 		if (IdleInTransactionSessionTimeout > 0)
+		{
+			INJECTION_POINT("idle-in-transaction-session-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT),
 					 errmsg("terminating connection due to idle-in-transaction timeout")));
+		}
 		else
 			IdleInTransactionSessionTimeoutPending = false;
 	}
@@ -3422,9 +3426,12 @@ ProcessInterrupts(void)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
 		if (TransactionTimeout > 0)
+		{
+			INJECTION_POINT("transaction-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
 					 errmsg("terminating connection due to transaction timeout")));
+		}
 		else
 			TransactionTimeoutPending = false;
 	}
@@ -3433,9 +3440,12 @@ ProcessInterrupts(void)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
 		if (IdleSessionTimeout > 0)
+		{
+			INJECTION_POINT("idle-session-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_IDLE_SESSION_TIMEOUT),
 					 errmsg("terminating connection due to idle-session timeout")));
+		}
 		else
 			IdleSessionTimeoutPending = false;
 	}
diff --git a/src/test/modules/test_misc/Makefile b/src/test/modules/test_misc/Makefile
index 39c6c2014a0..a958d156f47 100644
--- a/src/test/modules/test_misc/Makefile
+++ b/src/test/modules/test_misc/Makefile
@@ -2,6 +2,10 @@
 
 TAP_TESTS = 1
 
+EXTRA_INSTALL=src/test/modules/injection_points
+
+export enable_injection_points enable_injection_points
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 964d95db263..df2913e8938 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -5,11 +5,15 @@ tests += {
   'sd': meson.current_source_dir(),
   'bd': meson.current_build_dir(),
   'tap': {
+    'env': {
+       'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+    },
     'tests': [
       't/001_constraint_validation.pl',
       't/002_tablespace.pl',
       't/003_check_guc.pl',
       't/004_io_direct.pl',
+      't/005_timeouts.pl'
     ],
   },
 }
diff --git a/src/test/modules/test_misc/t/005_timeouts.pl b/src/test/modules/test_misc/t/005_timeouts.pl
new file mode 100644
index 00000000000..4768fbe750f
--- /dev/null
+++ b/src/test/modules/test_misc/t/005_timeouts.pl
@@ -0,0 +1,128 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Time::HiRes qw(usleep);
+use Test::More;
+
+# Test timeouts that will cause FATAL errors.  This test relies on injection
+# points to await a timeout occurrence. Relying on sleep proved to be unstable
+# on buildfarm. It's difficult to rely on the NOTICE injection point because
+# the backend under FATAL error can behave differently.
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# Node initialization
+my $node = PostgreSQL::Test::Cluster->new('master');
+$node->init();
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+#
+# 1. Test of the transaction timeout
+#
+
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('transaction-timeout', 'wait');");
+
+my $psql_session = $node->background_psql('postgres');
+
+# The following query will generate a stream of SELECT 1 queries. This is done
+# so to exercise transaction timeout in the presence of short queries.
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET transaction_timeout to '10ms';
+   BEGIN;
+   SELECT 1 \watch 0.001
+   \q
+));
+
+# Wait until the backend is in the timeout injection point. Will get an error
+# here if anything goes wrong.
+$node->wait_for_event('client backend', 'transaction-timeout');
+
+my $log_offset = -s $node->logfile;
+
+# Remove the injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('transaction-timeout');");
+
+# Check that the timeout was logged.
+$node->wait_for_log('terminating connection due to transaction timeout', $log_offset);
+
+# If we send \q with $psql_session->quit it can get to pump already closed.
+# So \q is in initial script, here we only finish IPC::Run.
+$psql_session->{run}->finish;
+
+
+#
+# 2. Test of the idle in transaction timeout
+#
+
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('idle-in-transaction-session-timeout', 'wait');"
+);
+
+# We begin a transaction and the hand on the line
+$psql_session = $node->background_psql('postgres');
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET idle_in_transaction_session_timeout to '10ms';
+   BEGIN;
+));
+
+# Wait until the backend is in the timeout injection point.
+$node->wait_for_event('client backend',
+	'idle-in-transaction-session-timeout');
+
+$log_offset = -s $node->logfile;
+
+# Remove the injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('idle-in-transaction-session-timeout');");
+
+# Check that the timeout was logged.
+$node->wait_for_log(
+	'terminating connection due to idle-in-transaction timeout', $log_offset);
+
+ok($psql_session->quit);
+
+
+#
+# 3. Test of the idle session timeout
+#
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('idle-session-timeout', 'wait');");
+
+# We just initialize the GUC and wait. No transaction is required.
+$psql_session = $node->background_psql('postgres');
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET idle_session_timeout to '10ms';
+));
+
+# Wait until the backend is in the timeout injection point.
+$node->wait_for_event('client backend', 'idle-session-timeout');
+
+$log_offset = -s $node->logfile;
+
+# Remove the injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('idle-session-timeout');");
+
+# Check that the timeout was logged.
+$node->wait_for_log(
+	'terminating connection due to idle-session timeout', $log_offset);
+
+ok($psql_session->quit);
+
+done_testing();
-- 
2.42.0



^ permalink  raw  reply  [nested|flat] 76+ messages in thread

* Re: Transaction timeout
@ 2024-03-13 08:17  Alexander Korotkov <[email protected]>
  parent: Andrey M. Borodin <[email protected]>
  0 siblings, 0 replies; 76+ messages in thread

From: Alexander Korotkov @ 2024-03-13 08:17 UTC (permalink / raw)
  To: Andrey M. Borodin <[email protected]>; +Cc: Japin Li <[email protected]>; Peter Smith <[email protected]>; Junwang Zhao <[email protected]>; 邱宇航 <[email protected]>; Fujii Masao <[email protected]>; Andrew Borodin <[email protected]>; Michael Paquier <[email protected]>; Nikolay Samokhvalov <[email protected]>; pgsql-hackers; pgsql-hackers mailing list <[email protected]>; Andres Freund <[email protected]>

On Wed, Mar 13, 2024 at 7:56 AM Andrey M. Borodin <[email protected]> wrote:
> > On 13 Mar 2024, at 05:23, Alexander Korotkov <[email protected]> wrote:
> >
> > On Tue, Mar 12, 2024 at 10:28 AM Andrey M. Borodin <[email protected]> wrote:
> >>> On 11 Mar 2024, at 16:18, Alexander Korotkov <[email protected]> wrote:
> >>>
> >>> I think if checking psql stderr is problematic, checking just logs is
> >>> fine.  Could we wait for the relevant log messages one by one with
> >>> $node->wait_for_log() just like 040_standby_failover_slots_sync.pl do?
> >>
> >> PFA version with $node->wait_for_log()
> >
> > I've slightly revised the patch.  I'm going to push it if no objections.
>
> One small note: log_offset was updated, but was not used.

Thank you. This is the updated version.

------
Regards,
Alexander Korotkov


Attachments:

  [application/octet-stream] v7-0001-Add-TAP-tests-for-timeouts.patch (7.5K, ../../CAPpHfdtQqNaiq-1kfnRhCPvuiExdDvS3gAO72nNOVv6D+018pQ@mail.gmail.com/2-v7-0001-Add-TAP-tests-for-timeouts.patch)
  download | inline diff:
From 5dddf46bd9fa7b7b9de5a914f24eacee89fee0a3 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email protected]>
Date: Wed, 13 Mar 2024 01:54:31 +0200
Subject: [PATCH v7] Add TAP tests for timeouts

This commit adds new tests to verify that transaction_timeout,
idle_session_timeout, and idle_in_transaction_session_timeout work as expected.
We introduce new injection points in before throwing a timeout FATAL error
and check these injection points are reached.

Discussion: https://postgr.es/m/CAAhFRxiQsRs2Eq5kCo9nXE3HTugsAAJdSQSmxncivebAxdmBjQ%40mail.gmail.com
Author: Andrey Borodin
Reviewed-by: Alexander Korotkov
---
 src/backend/tcop/postgres.c                  |  10 ++
 src/test/modules/test_misc/Makefile          |   4 +
 src/test/modules/test_misc/meson.build       |   4 +
 src/test/modules/test_misc/t/005_timeouts.pl | 129 +++++++++++++++++++
 4 files changed, 147 insertions(+)
 create mode 100644 src/test/modules/test_misc/t/005_timeouts.pl

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 6b7903314ab..7ac623019bc 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -72,6 +72,7 @@
 #include "tcop/tcopprot.h"
 #include "tcop/utility.h"
 #include "utils/guc_hooks.h"
+#include "utils/injection_point.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
@@ -3411,9 +3412,12 @@ ProcessInterrupts(void)
 		 * interrupt.
 		 */
 		if (IdleInTransactionSessionTimeout > 0)
+		{
+			INJECTION_POINT("idle-in-transaction-session-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT),
 					 errmsg("terminating connection due to idle-in-transaction timeout")));
+		}
 		else
 			IdleInTransactionSessionTimeoutPending = false;
 	}
@@ -3422,9 +3426,12 @@ ProcessInterrupts(void)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
 		if (TransactionTimeout > 0)
+		{
+			INJECTION_POINT("transaction-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
 					 errmsg("terminating connection due to transaction timeout")));
+		}
 		else
 			TransactionTimeoutPending = false;
 	}
@@ -3433,9 +3440,12 @@ ProcessInterrupts(void)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
 		if (IdleSessionTimeout > 0)
+		{
+			INJECTION_POINT("idle-session-timeout");
 			ereport(FATAL,
 					(errcode(ERRCODE_IDLE_SESSION_TIMEOUT),
 					 errmsg("terminating connection due to idle-session timeout")));
+		}
 		else
 			IdleSessionTimeoutPending = false;
 	}
diff --git a/src/test/modules/test_misc/Makefile b/src/test/modules/test_misc/Makefile
index 39c6c2014a0..a958d156f47 100644
--- a/src/test/modules/test_misc/Makefile
+++ b/src/test/modules/test_misc/Makefile
@@ -2,6 +2,10 @@
 
 TAP_TESTS = 1
 
+EXTRA_INSTALL=src/test/modules/injection_points
+
+export enable_injection_points enable_injection_points
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 964d95db263..df2913e8938 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -5,11 +5,15 @@ tests += {
   'sd': meson.current_source_dir(),
   'bd': meson.current_build_dir(),
   'tap': {
+    'env': {
+       'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+    },
     'tests': [
       't/001_constraint_validation.pl',
       't/002_tablespace.pl',
       't/003_check_guc.pl',
       't/004_io_direct.pl',
+      't/005_timeouts.pl'
     ],
   },
 }
diff --git a/src/test/modules/test_misc/t/005_timeouts.pl b/src/test/modules/test_misc/t/005_timeouts.pl
new file mode 100644
index 00000000000..e67b3e694b9
--- /dev/null
+++ b/src/test/modules/test_misc/t/005_timeouts.pl
@@ -0,0 +1,129 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Time::HiRes qw(usleep);
+use Test::More;
+
+# Test timeouts that will cause FATAL errors.  This test relies on injection
+# points to await a timeout occurrence. Relying on sleep proved to be unstable
+# on buildfarm. It's difficult to rely on the NOTICE injection point because
+# the backend under FATAL error can behave differently.
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# Node initialization
+my $node = PostgreSQL::Test::Cluster->new('master');
+$node->init();
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+#
+# 1. Test of the transaction timeout
+#
+
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('transaction-timeout', 'wait');");
+
+my $psql_session = $node->background_psql('postgres');
+
+# The following query will generate a stream of SELECT 1 queries. This is done
+# so to exercise transaction timeout in the presence of short queries.
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET transaction_timeout to '10ms';
+   BEGIN;
+   SELECT 1 \watch 0.001
+   \q
+));
+
+# Wait until the backend is in the timeout injection point. Will get an error
+# here if anything goes wrong.
+$node->wait_for_event('client backend', 'transaction-timeout');
+
+my $log_offset = -s $node->logfile;
+
+# Remove the injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('transaction-timeout');");
+
+# Check that the timeout was logged.
+$node->wait_for_log('terminating connection due to transaction timeout',
+	$log_offset);
+
+# If we send \q with $psql_session->quit it can get to pump already closed.
+# So \q is in initial script, here we only finish IPC::Run.
+$psql_session->{run}->finish;
+
+
+#
+# 2. Test of the sidle in transaction timeout
+#
+
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('idle-in-transaction-session-timeout', 'wait');"
+);
+
+# We begin a transaction and the hand on the line
+$psql_session = $node->background_psql('postgres');
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET idle_in_transaction_session_timeout to '10ms';
+   BEGIN;
+));
+
+# Wait until the backend is in the timeout injection point.
+$node->wait_for_event('client backend',
+	'idle-in-transaction-session-timeout');
+
+$log_offset = -s $node->logfile;
+
+# Remove the injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('idle-in-transaction-session-timeout');");
+
+# Check that the timeout was logged.
+$node->wait_for_log(
+	'terminating connection due to idle-in-transaction timeout', $log_offset);
+
+ok($psql_session->quit);
+
+
+#
+# 3. Test of the idle session timeout
+#
+$node->safe_psql('postgres',
+	"SELECT injection_points_attach('idle-session-timeout', 'wait');");
+
+# We just initialize the GUC and wait. No transaction is required.
+$psql_session = $node->background_psql('postgres');
+$psql_session->query_until(
+	qr/starting_bg_psql/, q(
+   \echo starting_bg_psql
+   SET idle_session_timeout to '10ms';
+));
+
+# Wait until the backend is in the timeout injection point.
+$node->wait_for_event('client backend', 'idle-session-timeout');
+
+$log_offset = -s $node->logfile;
+
+# Remove the injection point.
+$node->safe_psql('postgres',
+	"SELECT injection_points_wakeup('idle-session-timeout');");
+
+# Check that the timeout was logged.
+$node->wait_for_log('terminating connection due to idle-session timeout',
+	$log_offset);
+
+ok($psql_session->quit);
+
+done_testing();
-- 
2.39.3 (Apple Git-145)



^ permalink  raw  reply  [nested|flat] 76+ messages in thread


end of thread, other threads:[~2024-03-13 08:17 UTC | newest]

Thread overview: 76+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2001-03-17 01:10 transaction timeout Kevin T. Manley <[email protected]>
2019-09-03 22:17 [PATCH v3] Fix a delay in PostgreSQL shutdown caused by logical replication Alvaro Herrera <[email protected]>
2022-12-03 05:18 Transaction timeout Andrey Borodin <[email protected]>
2022-12-03 06:59 ` Re: Transaction timeout Nikolay Samokhvalov <[email protected]>
2022-12-03 17:41   ` Re: Transaction timeout Andrey Borodin <[email protected]>
2022-12-05 22:17     ` Re: Transaction timeout Nikolay Samokhvalov <[email protected]>
2022-12-05 22:28       ` Re: Transaction timeout Nikolay Samokhvalov <[email protected]>
2022-12-05 23:07     ` Re: Transaction timeout Andres Freund <[email protected]>
2022-12-05 23:41       ` Re: Transaction timeout Andrey Borodin <[email protected]>
2022-12-06 00:22         ` Re: Transaction timeout Andres Freund <[email protected]>
2022-12-06 00:44       ` Re: Transaction timeout Kyotaro Horiguchi <[email protected]>
2022-12-06 01:10         ` Re: Transaction timeout Andres Freund <[email protected]>
2022-12-07 02:52           ` Re: Transaction timeout Kyotaro Horiguchi <[email protected]>
2022-12-07 18:23     ` Re: Transaction timeout Andres Freund <[email protected]>
2022-12-07 21:30       ` Re: Transaction timeout Andrey Borodin <[email protected]>
2022-12-18 20:53         ` Re: Transaction timeout Andrey Borodin <[email protected]>
2023-12-18 05:49           ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2023-12-18 09:32             ` Re: Transaction timeout Japin Li <[email protected]>
2023-12-18 09:40               ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2023-12-19 01:25                 ` Re: Transaction timeout Japin Li <[email protected]>
2023-12-19 08:26                   ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2023-12-19 10:27                     ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2023-12-19 14:06                       ` Re: Transaction timeout Japin Li <[email protected]>
2023-12-22 05:39                         ` Re: Transaction timeout Japin Li <[email protected]>
2023-12-22 12:29                           ` Re: Transaction timeout Junwang Zhao <[email protected]>
2023-12-22 14:25                             ` Re: Transaction timeout Japin Li <[email protected]>
2023-12-22 14:37                               ` Re: Transaction timeout Junwang Zhao <[email protected]>
2023-12-22 14:44                                 ` Re: Transaction timeout Japin Li <[email protected]>
2023-12-22 15:30                                   ` Re: Transaction timeout Junwang Zhao <[email protected]>
2023-12-23 00:32                                     ` Re: Transaction timeout Japin Li <[email protected]>
2023-12-23 02:40                                       ` Re: Transaction timeout Japin Li <[email protected]>
2023-12-23 03:08                                         ` Re: Transaction timeout Junwang Zhao <[email protected]>
2023-12-23 03:17                                         ` Re: Transaction timeout Japin Li <[email protected]>
2023-12-23 03:35                                           ` Re: Transaction timeout Junwang Zhao <[email protected]>
2023-12-23 06:48                                             ` Re: Transaction timeout Li Japin <[email protected]>
2023-12-23 17:14                                               ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2023-12-25 02:17                                                 ` Re: Transaction timeout Junwang Zhao <[email protected]>
2023-12-25 02:27                                                 ` Re: Transaction timeout Japin Li <[email protected]>
2023-12-28 16:02                                                 ` Re: Transaction timeout Junwang Zhao <[email protected]>
2023-12-29 10:00                                                   ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2023-12-29 11:00                                                     ` Re: Transaction timeout Junwang Zhao <[email protected]>
2023-12-29 11:15                                                       ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2024-01-01 14:28                                                         ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2024-01-03 06:39                                                           ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2024-01-03 11:46                                                             ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2024-01-03 12:04                                                               ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2024-01-04 02:14                                                                 ` Re: Transaction timeout Japin Li <[email protected]>
2024-01-04 08:41                                                                   ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2024-01-22 06:23                                                                     ` Re: Transaction timeout Peter Smith <[email protected]>
2024-01-26 06:44                                                                       ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2024-01-26 06:46                                                                         ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2024-01-26 14:58                                                                         ` Re: Transaction timeout Japin Li <[email protected]>
2024-01-30 06:22                                                                           ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2024-01-31 09:27                                                                             ` Re: Transaction timeout Japin Li <[email protected]>
2024-01-31 09:57                                                                               ` Re: Transaction timeout Andrey Borodin <[email protected]>
2024-02-13 21:42                                                                                 ` Re: Transaction timeout Alexander Korotkov <[email protected]>
2024-02-15 23:08                                                                                   ` Re: Transaction timeout Andres Freund <[email protected]>
2024-02-18 19:16                                                                                     ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2024-02-19 09:14                                                                                       ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2024-02-19 10:17                                                                                         ` Re: Transaction timeout Japin Li <[email protected]>
2024-02-22 17:23                                                                                           ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2024-02-25 18:50                                                                                             ` Re: Transaction timeout Alexander Korotkov <[email protected]>
2024-03-06 08:22                                                                                               ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2024-03-06 19:55                                                                                                 ` Re: Transaction timeout Alexander Korotkov <[email protected]>
2024-03-11 10:52                                                                                                   ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2024-03-11 11:18                                                                                                     ` Re: Transaction timeout Alexander Korotkov <[email protected]>
2024-03-12 08:28                                                                                                       ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2024-03-13 00:23                                                                                                         ` Re: Transaction timeout Alexander Korotkov <[email protected]>
2024-03-13 05:56                                                                                                           ` Re: Transaction timeout Andrey M. Borodin <[email protected]>
2024-03-13 08:17                                                                                                             ` Re: Transaction timeout Alexander Korotkov <[email protected]>
2023-12-19 14:51                       ` Re: Transaction timeout Junwang Zhao <[email protected]>
2023-12-20 01:48                         ` Re: Transaction timeout Junwang Zhao <[email protected]>
2023-12-20 01:58                           ` 回复: Transaction timeout Thomas wen <[email protected]>
2023-12-20 02:35                             ` Re: Transaction timeout Junwang Zhao <[email protected]>
2023-12-20 12:23                               ` Re: Transaction timeout wenhui qiu <[email protected]>
2023-08-24 14:42 [PATCH v4 1/2] rename some pgstat functions Nathan Bossart <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox