I've updated the patches.

Adding the assertion actually turned up a corner case where RecentXmin
was *not* set. If you lock a temporary table and that's the only thing
you do in a transaction then the flag is set indicating you've used
the temp schema but you never take a snapshot :(

I also split out the warnings and added a test that relfrozenxid was
advanced on the toast table as well.

I haven't wrapped my head around multixacts yet. It's complicated by
this same codepath being used for truncates of regular tables that
were created in the same transaction.
From 63801cbb7c20676df98be47c52269bb6d7cf7b06 Mon Sep 17 00:00:00 2001
From: Greg Stark <st...@mit.edu>
Date: Thu, 31 Mar 2022 15:49:19 -0400
Subject: [PATCH v5 2/3] Update relfrozenxmin when truncating temp tables

Make ON COMMIT DELETE ROWS reset relfrozenxmin and other table stats
like normal truncate. Otherwise even typical short-lived transactions
using temporary tables can easily cause them to reach relfrozenxid.
---
 src/backend/catalog/heap.c | 60 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 60 insertions(+)

diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 6eb78a9c0f..3f593f03dc 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -30,6 +30,7 @@
 #include "postgres.h"
 
 #include "access/genam.h"
+#include "access/heapam.h"
 #include "access/multixact.h"
 #include "access/relation.h"
 #include "access/table.h"
@@ -70,6 +71,7 @@
 #include "utils/fmgroids.h"
 #include "utils/inval.h"
 #include "utils/lsyscache.h"
+#include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
 
@@ -2980,6 +2982,54 @@ RelationTruncateIndexes(Relation heapRelation)
 	}
 }
 
+/*
+ * Reset the relfrozenxid and other stats to the same values used when
+ * creating tables. This is used after non-transactional truncation.
+ *
+ * Doing this reduces the need for long-running programs to vacuum their own
+ * temporary tables (since they're not covered by autovacuum) at least in the
+ * case where they're ON COMMIT DELETE ROWS.
+ *
+ * see also src/backend/commands/vacuum.c vac_update_relstats()
+ * also see AddNewRelationTuple() above
+ */
+
+static void
+ResetVacStats(Relation rel)
+{
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+	Relation classRel;
+
+	/* Ensure RecentXmin is valid -- it almost certainly is but regression
+	 * tests turned up an unlikely corner case where it might not be */
+	if (!FirstSnapshotSet)
+		(void)GetLatestSnapshot();
+	Assert(FirstSnapshotSet);
+	
+	/* Fetch a copy of the tuple to scribble on */
+	classRel = table_open(RelationRelationId, RowExclusiveLock);
+	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(RelationGetRelid(rel)));
+	if (!HeapTupleIsValid(ctup))
+		elog(ERROR, "pg_class entry for relid %u vanished during truncation",
+			 RelationGetRelid(rel));
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	/*
+	 * Update relfrozenxid
+	 */
+
+	pgcform->relpages = 0;
+	pgcform->reltuples = -1;
+	pgcform->relallvisible = 0;
+	pgcform->relfrozenxid = RecentXmin;
+	pgcform->relminmxid = GetOldestMultiXactId();
+
+	heap_inplace_update(classRel, ctup);
+
+	table_close(classRel, RowExclusiveLock);
+}
+
 /*
  *	 heap_truncate
  *
@@ -2988,6 +3038,14 @@ RelationTruncateIndexes(Relation heapRelation)
  * This is not transaction-safe!  There is another, transaction-safe
  * implementation in commands/tablecmds.c.  We now use this only for
  * ON COMMIT truncation of temporary tables, where it doesn't matter.
+ *
+ * Or whenever a table's relfilenode was created within the same transaction
+ * such as when the table was created or truncated (normally) within this
+ * transaction.
+ *
+ * The correctness of this code depends on the fact that the table creation or
+ * truncation would be rolled back *including* the insert/update to the
+ * pg_class row that we update in place here.
  */
 void
 heap_truncate(List *relids)
@@ -3044,6 +3102,7 @@ heap_truncate_one_rel(Relation rel)
 
 	/* Truncate the underlying relation */
 	table_relation_nontransactional_truncate(rel);
+	ResetVacStats(rel);
 
 	/* If the relation has indexes, truncate the indexes too */
 	RelationTruncateIndexes(rel);
@@ -3055,6 +3114,7 @@ heap_truncate_one_rel(Relation rel)
 		Relation	toastrel = table_open(toastrelid, AccessExclusiveLock);
 
 		table_relation_nontransactional_truncate(toastrel);
+		ResetVacStats(toastrel);
 		RelationTruncateIndexes(toastrel);
 		/* keep the lock... */
 		table_close(toastrel, NoLock);
-- 
2.35.1

From 717c416a1bb26ac46ece04eb7b755d9ee12cb9c1 Mon Sep 17 00:00:00 2001
From: Greg Stark <st...@mit.edu>
Date: Thu, 31 Mar 2022 15:50:02 -0400
Subject: [PATCH v5 3/3] Add test for truncating temp tables advancing
 relfrozenxid

This test depends on other transactions not running at the same time
so that relfrozenxid can advance so it has to be moved to its own
schedule.
---
 src/test/regress/expected/temp.out | 37 ++++++++++++++++++++++++++++++
 src/test/regress/parallel_schedule | 10 +++++---
 src/test/regress/sql/temp.sql      | 30 ++++++++++++++++++++++++
 3 files changed, 74 insertions(+), 3 deletions(-)

diff --git a/src/test/regress/expected/temp.out b/src/test/regress/expected/temp.out
index a5b3ed34a3..244b868ef7 100644
--- a/src/test/regress/expected/temp.out
+++ b/src/test/regress/expected/temp.out
@@ -82,6 +82,43 @@ SELECT * FROM temptest;
 -----
 (0 rows)
 
+DROP TABLE temptest;
+-- Test that ON COMMIT DELETE ROWS resets the relfrozenxid when the
+-- table is truncated. This requires this test not be run in parallel
+-- with other tests as concurrent transactions will hold back the
+-- globalxmin
+CREATE TEMP TABLE temptest(col text) ON COMMIT DELETE ROWS;
+SELECT reltoastrelid, reltoastrelid::regclass AS relname FROM pg_class where oid = 'temptest'::regclass \gset toast_
+SELECT relpages, reltuples, relfrozenxid FROM pg_class where oid = 'temptest'::regclass \gset old_
+SELECT relpages, reltuples, relfrozenxid FROM pg_class where oid = :toast_reltoastrelid \gset toast_old_
+BEGIN;
+INSERT INTO temptest (select repeat('foobar',generate_series(1,1000)));
+ANALYZE temptest; -- update relpages, reltuples
+SELECT relpages, reltuples, relfrozenxid FROM pg_class where oid = 'temptest'::regclass \gset temp_
+SELECT relpages, reltuples, relfrozenxid FROM pg_class where oid = :toast_reltoastrelid \gset toast_temp_
+COMMIT;
+SELECT relpages, reltuples, relfrozenxid FROM pg_class where oid = 'temptest'::regclass \gset new_
+SELECT relpages, reltuples, relfrozenxid FROM pg_class where oid = :toast_reltoastrelid \gset toast_new_
+-- make sure relpages and reltuple match a newly created table and
+-- relfrozenxid is advanced
+SELECT :old_relpages     <> :temp_relpages     AS pages_analyzed,
+       :old_relpages      = :new_relpages      AS pages_reset,
+       :old_reltuples    <> :temp_reltuples    AS tuples_analyzed,
+       :old_reltuples     = :new_reltuples     AS tuples_reset,
+       :old_relfrozenxid <> :new_relfrozenxid  AS frozenxid_advanced;
+ pages_analyzed | pages_reset | tuples_analyzed | tuples_reset | frozenxid_advanced 
+----------------+-------------+-----------------+--------------+--------------------
+ t              | t           | t               | t            | t
+(1 row)
+
+-- The toast table can't be analyzed so relpages and reltuples can't
+-- be tested easily make sure frozenxid is advanced
+SELECT :toast_old_relfrozenxid <> :toast_new_relfrozenxid  AS frozenxid_advanced;
+ frozenxid_advanced 
+--------------------
+ t
+(1 row)
+
 DROP TABLE temptest;
 -- Test ON COMMIT DROP
 BEGIN;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5030d19c03..fd86ce5b5c 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -116,10 +116,14 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson
 # ----------
 # Another group of parallel tests
 # with depends on create_misc
-# NB: temp.sql does a reconnect which transiently uses 2 connections,
-# so keep this parallel group to at most 19 tests
 # ----------
-test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml
+test: plancache limit plpgsql copy2 domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml
+
+# ----------
+# Run this alone because it transiently uses 2 connections and also
+# tests relfrozenxid advances when truncating temp tables
+# ----------
+test: temp
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/temp.sql b/src/test/regress/sql/temp.sql
index 424d12b283..5f8647a8aa 100644
--- a/src/test/regress/sql/temp.sql
+++ b/src/test/regress/sql/temp.sql
@@ -79,6 +79,36 @@ SELECT * FROM temptest;
 
 DROP TABLE temptest;
 
+-- Test that ON COMMIT DELETE ROWS resets the relfrozenxid when the
+-- table is truncated. This requires this test not be run in parallel
+-- with other tests as concurrent transactions will hold back the
+-- globalxmin
+CREATE TEMP TABLE temptest(col text) ON COMMIT DELETE ROWS;
+
+SELECT reltoastrelid, reltoastrelid::regclass AS relname FROM pg_class where oid = 'temptest'::regclass \gset toast_
+SELECT relpages, reltuples, relfrozenxid FROM pg_class where oid = 'temptest'::regclass \gset old_
+SELECT relpages, reltuples, relfrozenxid FROM pg_class where oid = :toast_reltoastrelid \gset toast_old_
+BEGIN;
+INSERT INTO temptest (select repeat('foobar',generate_series(1,1000)));
+ANALYZE temptest; -- update relpages, reltuples
+SELECT relpages, reltuples, relfrozenxid FROM pg_class where oid = 'temptest'::regclass \gset temp_
+SELECT relpages, reltuples, relfrozenxid FROM pg_class where oid = :toast_reltoastrelid \gset toast_temp_
+COMMIT;
+SELECT relpages, reltuples, relfrozenxid FROM pg_class where oid = 'temptest'::regclass \gset new_
+SELECT relpages, reltuples, relfrozenxid FROM pg_class where oid = :toast_reltoastrelid \gset toast_new_
+-- make sure relpages and reltuple match a newly created table and
+-- relfrozenxid is advanced
+SELECT :old_relpages     <> :temp_relpages     AS pages_analyzed,
+       :old_relpages      = :new_relpages      AS pages_reset,
+       :old_reltuples    <> :temp_reltuples    AS tuples_analyzed,
+       :old_reltuples     = :new_reltuples     AS tuples_reset,
+       :old_relfrozenxid <> :new_relfrozenxid  AS frozenxid_advanced;
+-- The toast table can't be analyzed so relpages and reltuples can't
+-- be tested easily make sure frozenxid is advanced
+SELECT :toast_old_relfrozenxid <> :toast_new_relfrozenxid  AS frozenxid_advanced;
+
+DROP TABLE temptest;
+
 -- Test ON COMMIT DROP
 
 BEGIN;
-- 
2.35.1

From c692935f8106f4e0c70eb0e5a0ab1f56c06ffc18 Mon Sep 17 00:00:00 2001
From: Greg Stark <st...@mit.edu>
Date: Thu, 31 Mar 2022 15:48:38 -0400
Subject: [PATCH v5 1/3] Add warnings when old temporary tables are found to
 still be in use during autovacuum. Long lived sessions using temporary tables
 are required to vacuum them themselves.

For the warning to be useful modify checkTempNamespaceStatus to
return the backend pid using it so that we can inform super-user
which pid to terminate. Otherwise it's quite tricky to determine
as a user. Rename the function to avoid an incompatible ABI break.
---
 src/backend/access/transam/varsup.c | 12 ++++---
 src/backend/catalog/namespace.c     |  9 +++--
 src/backend/postmaster/autovacuum.c | 52 ++++++++++++++++++++++-------
 src/include/catalog/namespace.h     |  4 +--
 4 files changed, 57 insertions(+), 20 deletions(-)

diff --git a/src/backend/access/transam/varsup.c b/src/backend/access/transam/varsup.c
index 748120a012..8b29573e9f 100644
--- a/src/backend/access/transam/varsup.c
+++ b/src/backend/access/transam/varsup.c
@@ -129,14 +129,16 @@ GetNewTransactionId(bool isSubXact)
 						 errmsg("database is not accepting commands to avoid wraparound data loss in database \"%s\"",
 								oldest_datname),
 						 errhint("Stop the postmaster and vacuum that database in single-user mode.\n"
-								 "You might also need to commit or roll back old prepared transactions, or drop stale replication slots.")));
+								 "You might also need to commit or roll back old prepared transactions,\n"
+								 "drop temporary tables, or drop stale replication slots.")));
 			else
 				ereport(ERROR,
 						(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
 						 errmsg("database is not accepting commands to avoid wraparound data loss in database with OID %u",
 								oldest_datoid),
 						 errhint("Stop the postmaster and vacuum that database in single-user mode.\n"
-								 "You might also need to commit or roll back old prepared transactions, or drop stale replication slots.")));
+								 "You might also need to commit or roll back old prepared transactions,\n"
+								 "drop temporary tables, or drop stale replication slots.")));
 		}
 		else if (TransactionIdFollowsOrEquals(xid, xidWarnLimit))
 		{
@@ -149,14 +151,16 @@ GetNewTransactionId(bool isSubXact)
 								oldest_datname,
 								xidWrapLimit - xid),
 						 errhint("To avoid a database shutdown, execute a database-wide VACUUM in that database.\n"
-								 "You might also need to commit or roll back old prepared transactions, or drop stale replication slots.")));
+								 "You might also need to commit or roll back old prepared transactions,\n"
+								 "drop temporary tables, or drop stale replication slots.")));
 			else
 				ereport(WARNING,
 						(errmsg("database with OID %u must be vacuumed within %u transactions",
 								oldest_datoid,
 								xidWrapLimit - xid),
 						 errhint("To avoid a database shutdown, execute a database-wide VACUUM in that database.\n"
-								 "You might also need to commit or roll back old prepared transactions, or drop stale replication slots.")));
+								 "You might also need to commit or roll back old prepared transactions,\n"
+								 "drop temporary tables, or drop stale replication slots.")));
 		}
 
 		/* Re-acquire lock and start over */
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index fafb9349cc..c1fd3ced95 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -3272,15 +3272,18 @@ isOtherTempNamespace(Oid namespaceId)
 
 /*
  * checkTempNamespaceStatus - is the given namespace owned and actively used
- * by a backend?
+ * by a backend? Optionally return the pid of the owning backend if there is
+ * one. Returned pid is only meaningful when TEMP_NAMESPACE_IN_USE but note
+ * below about race conditions.
  *
  * Note: this can be used while scanning relations in pg_class to detect
  * orphaned temporary tables or namespaces with a backend connected to a
  * given database.  The result may be out of date quickly, so the caller
  * must be careful how to handle this information.
+ *
  */
 TempNamespaceStatus
-checkTempNamespaceStatus(Oid namespaceId)
+checkTempNamespaceStatusAndPid(Oid namespaceId, pid_t *pid)
 {
 	PGPROC	   *proc;
 	int			backendId;
@@ -3307,6 +3310,8 @@ checkTempNamespaceStatus(Oid namespaceId)
 		return TEMP_NAMESPACE_IDLE;
 
 	/* Yup, so namespace is busy */
+	if (pid)
+		*pid = proc->pid;
 	return TEMP_NAMESPACE_IN_USE;
 }
 
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 681ef91b81..19e569a693 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2085,6 +2085,8 @@ do_autovacuum(void)
 		bool		dovacuum;
 		bool		doanalyze;
 		bool		wraparound;
+		TempNamespaceStatus temp_status;
+		pid_t		temp_pid;
 
 		if (classForm->relkind != RELKIND_RELATION &&
 			classForm->relkind != RELKIND_MATVIEW)
@@ -2092,6 +2094,16 @@ do_autovacuum(void)
 
 		relid = classForm->oid;
 
+		/* Fetch reloptions and the pgstat entry for this table */
+		relopts = extract_autovac_opts(tuple, pg_class_desc);
+		tabentry = get_pgstat_tabentry_relid(relid, classForm->relisshared,
+											 shared, dbentry);
+
+		/* Check if it needs vacuum or analyze */
+		relation_needs_vacanalyze(relid, relopts, classForm, tabentry,
+								  effective_multixact_freeze_max_age,
+								  &dovacuum, &doanalyze, &wraparound);
+
 		/*
 		 * Check if it is a temp table (presumably, of some other backend's).
 		 * We cannot safely process other backends' temp tables.
@@ -2103,7 +2115,8 @@ do_autovacuum(void)
 			 * using the temporary schema.  Also, for safety, ignore it if the
 			 * namespace doesn't exist or isn't a temp namespace after all.
 			 */
-			if (checkTempNamespaceStatus(classForm->relnamespace) == TEMP_NAMESPACE_IDLE)
+			temp_status = checkTempNamespaceStatusAndPid(classForm->relnamespace, &temp_pid);
+			if (temp_status == TEMP_NAMESPACE_IDLE)
 			{
 				/*
 				 * The table seems to be orphaned -- although it might be that
@@ -2114,19 +2127,34 @@ do_autovacuum(void)
 				 */
 				orphan_oids = lappend_oid(orphan_oids, relid);
 			}
+			else if (temp_status == TEMP_NAMESPACE_NOT_TEMP)
+			{
+				elog(LOG, "autovacuum: found temporary table \"%s.%s.%s\" in non-temporary namespace",
+					 get_database_name(MyDatabaseId),
+					 get_namespace_name(classForm->relnamespace),
+					 NameStr(classForm->relname));
+			}
+			else if (temp_status == TEMP_NAMESPACE_IN_USE && wraparound)
+			{
+				/* The table is not orphaned -- however it seems to be in need
+				 * of a wraparound vacuum which we cannot do. Sessions using
+				 * long-lived temporary tables need to be responsible for
+				 * vacuuming them and failing to do so is endangering the
+				 * whole cluster.
+				 */
+				ereport(LOG,
+						(errmsg("autovacuum: cannot vacuum temporary table \"%s.%s.%s\" in danger of causing transaction wraparound",
+								get_database_name(MyDatabaseId),
+								get_namespace_name(classForm->relnamespace),
+								NameStr(classForm->relname)),
+						 errhint("Long-lived clients must vacuum temporary tables themselves periodically.\n"
+								 "As super-user drop this table or terminate this session with pg_terminate_backend(%lu).",
+								 (unsigned long)temp_pid)
+							));
+			}
 			continue;
 		}
 
-		/* Fetch reloptions and the pgstat entry for this table */
-		relopts = extract_autovac_opts(tuple, pg_class_desc);
-		tabentry = get_pgstat_tabentry_relid(relid, classForm->relisshared,
-											 shared, dbentry);
-
-		/* Check if it needs vacuum or analyze */
-		relation_needs_vacanalyze(relid, relopts, classForm, tabentry,
-								  effective_multixact_freeze_max_age,
-								  &dovacuum, &doanalyze, &wraparound);
-
 		/* Relations that need work are added to table_oids */
 		if (dovacuum || doanalyze)
 			table_oids = lappend_oid(table_oids, relid);
@@ -2273,7 +2301,7 @@ do_autovacuum(void)
 			continue;
 		}
 
-		if (checkTempNamespaceStatus(classForm->relnamespace) != TEMP_NAMESPACE_IDLE)
+		if (checkTempNamespaceStatusAndPid(classForm->relnamespace, NULL) != TEMP_NAMESPACE_IDLE)
 		{
 			UnlockRelationOid(relid, AccessExclusiveLock);
 			continue;
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index f963d82797..a861327cd8 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -39,7 +39,7 @@ typedef struct _FuncCandidateList
 }		   *FuncCandidateList;
 
 /*
- * Result of checkTempNamespaceStatus
+ * Result of checkTempNamespaceStatusAndPid
  */
 typedef enum TempNamespaceStatus
 {
@@ -155,7 +155,7 @@ extern bool isTempToastNamespace(Oid namespaceId);
 extern bool isTempOrTempToastNamespace(Oid namespaceId);
 extern bool isAnyTempNamespace(Oid namespaceId);
 extern bool isOtherTempNamespace(Oid namespaceId);
-extern TempNamespaceStatus checkTempNamespaceStatus(Oid namespaceId);
+extern TempNamespaceStatus checkTempNamespaceStatusAndPid(Oid namespaceId, pid_t *pid);
 extern int	GetTempNamespaceBackendId(Oid namespaceId);
 extern Oid	GetTempToastNamespace(void);
 extern void GetTempNamespaceState(Oid *tempNamespaceId,
-- 
2.35.1

Reply via email to