optimizing pg_upgrade's once-in-each-database steps

Started by Nathan Bossartover 1 year ago37 messages
#1Nathan Bossart
nathandbossart@gmail.com
1 attachment(s)

A number of pg_upgrade steps require connecting to each database and
running a query. When there are many databases, these steps are
particularly time-consuming, especially since this is done sequentially in
a single process. At a quick glance, I see the following such steps:

* create_logical_replication_slots
* check_for_data_types_usage
* check_for_isn_and_int8_passing_mismatch
* check_for_user_defined_postfix_ops
* check_for_incompatible_polymorphics
* check_for_tables_with_oids
* check_for_user_defined_encoding_conversions
* check_old_cluster_subscription_state
* get_loadable_libraries
* get_db_rel_and_slot_infos
* old_9_6_invalidate_hash_indexes
* report_extension_updates

I set out to parallelize these kinds of steps via multiple threads or
processes, but I ended up realizing that we could likely achieve much of
the same gain with libpq's asynchronous APIs. Specifically, both
establishing the connections and running the queries can be done without
blocking, so we can just loop over a handful of slots and advance a simple
state machine for each. The attached is a proof-of-concept grade patch for
doing this for get_db_rel_and_slot_infos(), which yielded the following
results on my laptop for "pg_upgrade --link --sync-method=syncfs --jobs 8"
for a cluster with 10K empty databases.

total pg_upgrade_time:
* HEAD: 14m 8s
* patch: 10m 58s

get_db_rel_and_slot_infos() on old cluster:
* HEAD: 2m 45s
* patch: 36s

get_db_rel_and_slot_infos() on new cluster:
* HEAD: 1m 46s
* patch: 29s

I am posting this early to get thoughts on the general approach. If we
proceeded with this strategy, I'd probably create some generic tooling that
each relevant step would provide a set of callback functions.

--
Nathan Bossart
Amazon Web Services: https://aws.amazon.com

Attachments:

v1-0001-parallel-get-relinfos.patchtext/x-diff; charset=us-asciiDownload
From 05a9903295cb3b57ca9144217e89f0aac27277b5 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 15 May 2024 12:07:10 -0500
Subject: [PATCH v1 1/1] parallel get relinfos

---
 src/bin/pg_upgrade/info.c        | 266 +++++++++++++++++++++++--------
 src/tools/pgindent/typedefs.list |   1 +
 2 files changed, 202 insertions(+), 65 deletions(-)

diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 95c22a7200..bb28e262c7 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -11,6 +11,7 @@
 
 #include "access/transam.h"
 #include "catalog/pg_class_d.h"
+#include "fe_utils/string_utils.h"
 #include "pg_upgrade.h"
 
 static void create_rel_filename_map(const char *old_data, const char *new_data,
@@ -22,13 +23,16 @@ static void report_unmatched_relation(const RelInfo *rel, const DbInfo *db,
 static void free_db_and_rel_infos(DbInfoArr *db_arr);
 static void get_template0_info(ClusterInfo *cluster);
 static void get_db_infos(ClusterInfo *cluster);
-static void get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo);
+static void start_rel_infos_query(PGconn *conn);
+static void get_rel_infos_result(PGconn *conn, DbInfo *dbinfo);
 static void free_rel_infos(RelInfoArr *rel_arr);
 static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
-static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check);
-static void get_db_subscription_count(DbInfo *dbinfo);
+static void start_old_cluster_logical_slot_infos_query(PGconn *conn, bool live_check);
+static void get_old_cluster_logical_slot_infos_result(PGconn *conn, DbInfo *dbinfo);
+static void start_db_sub_count_query(PGconn *conn, DbInfo *dbinfo);
+static void get_db_sub_count_result(PGconn *conn, DbInfo *dbinfo);
 
 
 /*
@@ -268,6 +272,16 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
 			   reloid, db->db_name, reldesc);
 }
 
+typedef enum
+{
+	UNUSED,
+	CONN_STARTED,
+	CONNECTING,
+	STARTED_RELINFO_QUERY,
+	STARTED_LOGICAL_QUERY,
+	STARTED_SUBSCRIPTION_QUERY,
+} InfoState;
+
 /*
  * get_db_rel_and_slot_infos()
  *
@@ -279,7 +293,12 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
 void
 get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check)
 {
-	int			dbnum;
+	int			dbnum = 0;
+	int			dbnum_proc = 0;
+	InfoState  *states;
+	int		   *dbs;
+	PGconn	  **conns;
+	int			jobs = (user_opts.jobs < 1) ? 1 : user_opts.jobs;
 
 	if (cluster->dbarr.dbs != NULL)
 		free_db_and_rel_infos(&cluster->dbarr);
@@ -287,20 +306,103 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check)
 	get_template0_info(cluster);
 	get_db_infos(cluster);
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		DbInfo	   *pDbInfo = &cluster->dbarr.dbs[dbnum];
+	states = (InfoState *) pg_malloc(sizeof(InfoState *) * jobs);
+	dbs = (int *) pg_malloc(sizeof(int) * jobs);
+	conns = (PGconn **) pg_malloc(sizeof(PGconn *) * jobs);
 
-		get_rel_infos(cluster, pDbInfo);
+	for (int i = 0; i < jobs; i++)
+		states[i] = UNUSED;
 
-		/*
-		 * Retrieve the logical replication slots infos and the subscriptions
-		 * count for the old cluster.
-		 */
-		if (cluster == &old_cluster)
+	while (dbnum < cluster->dbarr.ndbs)
+	{
+		for (int i = 0; i < jobs; i++)
 		{
-			get_old_cluster_logical_slot_infos(pDbInfo, live_check);
-			get_db_subscription_count(pDbInfo);
+			switch (states[i])
+			{
+				case UNUSED:
+					if (dbnum_proc < cluster->dbarr.ndbs)
+					{
+						PQExpBufferData conn_opts;
+
+						dbs[i] = dbnum_proc++;
+
+						/* Build connection string with proper quoting */
+						initPQExpBuffer(&conn_opts);
+						appendPQExpBufferStr(&conn_opts, "dbname=");
+						appendConnStrVal(&conn_opts, cluster->dbarr.dbs[dbs[i]].db_name);
+						appendPQExpBufferStr(&conn_opts, " user=");
+						appendConnStrVal(&conn_opts, os_info.user);
+						appendPQExpBuffer(&conn_opts, " port=%d", cluster->port);
+						if (cluster->sockdir)
+						{
+							appendPQExpBufferStr(&conn_opts, " host=");
+							appendConnStrVal(&conn_opts, cluster->sockdir);
+						}
+
+						conns[i] = PQconnectStart(conn_opts.data);
+						termPQExpBuffer(&conn_opts);
+						states[i] = CONNECTING;
+					}
+					break;
+				case CONNECTING:
+					if (PQconnectPoll(conns[i]) == PGRES_POLLING_FAILED)
+					{
+						pg_log(PG_REPORT, "%s", PQerrorMessage(conns[i]));
+						exit(1);
+					}
+					if (PQconnectPoll(conns[i]) == PGRES_POLLING_OK)
+						states[i] = CONN_STARTED;
+					break;
+				case CONN_STARTED:
+					if (PQstatus(conns[i]) == CONNECTION_OK)
+					{
+						start_rel_infos_query(conns[i]);
+						states[i] = STARTED_RELINFO_QUERY;
+					}
+					break;
+				case STARTED_RELINFO_QUERY:
+					if (PQisBusy(conns[i]))
+						PQconsumeInput(conns[i]);
+					else
+					{
+						get_rel_infos_result(conns[i], &cluster->dbarr.dbs[dbs[i]]);
+
+						if (cluster == &old_cluster &&
+							GET_MAJOR_VERSION(old_cluster.major_version) >= 1700)
+						{
+							start_old_cluster_logical_slot_infos_query(conns[i], live_check);
+							states[i] = STARTED_LOGICAL_QUERY;
+						}
+						else
+						{
+							dbnum++;
+							PQfinish(conns[i]);
+							states[i] = UNUSED;
+						}
+					}
+					break;
+				case STARTED_LOGICAL_QUERY:
+					if (PQisBusy(conns[i]))
+						PQconsumeInput(conns[i]);
+					else
+					{
+						get_old_cluster_logical_slot_infos_result(conns[i], &cluster->dbarr.dbs[dbs[i]]);
+						start_db_sub_count_query(conns[i], &cluster->dbarr.dbs[dbs[i]]);
+						states[i] = STARTED_SUBSCRIPTION_QUERY;
+					}
+					break;
+				case STARTED_SUBSCRIPTION_QUERY:
+					if (PQisBusy(conns[i]))
+						PQconsumeInput(conns[i]);
+					else
+					{
+						get_db_sub_count_result(conns[i], &cluster->dbarr.dbs[dbs[i]]);
+						dbnum++;
+						PQfinish(conns[i]);
+						states[i] = UNUSED;
+					}
+					break;
+			}
 		}
 	}
 
@@ -450,29 +552,9 @@ get_db_infos(ClusterInfo *cluster)
  * This allows later processing to match up old and new databases efficiently.
  */
 static void
-get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
+start_rel_infos_query(PGconn *conn)
 {
-	PGconn	   *conn = connectToServer(cluster,
-									   dbinfo->db_name);
-	PGresult   *res;
-	RelInfo    *relinfos;
-	int			ntups;
-	int			relnum;
-	int			num_rels = 0;
-	char	   *nspname = NULL;
-	char	   *relname = NULL;
-	char	   *tablespace = NULL;
-	int			i_spclocation,
-				i_nspname,
-				i_relname,
-				i_reloid,
-				i_indtable,
-				i_toastheap,
-				i_relfilenumber,
-				i_reltablespace;
 	char		query[QUERY_ALLOC];
-	char	   *last_namespace = NULL,
-			   *last_tablespace = NULL;
 
 	query[0] = '\0';			/* initialize query string to empty */
 
@@ -552,7 +634,38 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 			 "     ON c.reltablespace = t.oid "
 			 "ORDER BY 1;");
 
-	res = executeQueryOrDie(conn, "%s", query);
+	if (PQsendQuery(conn, query) == 0)
+	{
+		/* TODO: fail */
+	}
+}
+
+static void
+get_rel_infos_result(PGconn *conn, DbInfo *dbinfo)
+{
+	PGresult   *res = PQgetResult(conn);
+	RelInfo    *relinfos;
+	int			ntups;
+	int			relnum;
+	int			num_rels = 0;
+	char	   *nspname = NULL;
+	char	   *relname = NULL;
+	char	   *tablespace = NULL;
+	int			i_spclocation,
+				i_nspname,
+				i_relname,
+				i_reloid,
+				i_indtable,
+				i_toastheap,
+				i_relfilenumber,
+				i_reltablespace;
+	char	   *last_namespace = NULL,
+			   *last_tablespace = NULL;
+
+	if (PQgetResult(conn) != NULL)
+	{
+		/* TODO: fail */
+	}
 
 	ntups = PQntuples(res);
 
@@ -622,8 +735,6 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	}
 	PQclear(res);
 
-	PQfinish(conn);
-
 	dbinfo->rel_arr.rels = relinfos;
 	dbinfo->rel_arr.nrels = num_rels;
 }
@@ -645,19 +756,14 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
  * are included.
  */
 static void
-get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
+start_old_cluster_logical_slot_infos_query(PGconn *conn, bool live_check)
 {
-	PGconn	   *conn;
-	PGresult   *res;
-	LogicalSlotInfo *slotinfos = NULL;
-	int			num_slots;
+	char		query[QUERY_ALLOC];
 
 	/* Logical slots can be migrated since PG17. */
 	if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1600)
 		return;
 
-	conn = connectToServer(&old_cluster, dbinfo->db_name);
-
 	/*
 	 * Fetch the logical replication slot information. The check whether the
 	 * slot is considered caught up is done by an upgrade function. This
@@ -675,16 +781,34 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
-							"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
-							"FROM pg_catalog.pg_replication_slots "
-							"WHERE slot_type = 'logical' AND "
-							"database = current_database() AND "
-							"temporary IS FALSE;",
-							live_check ? "FALSE" :
-							"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
-							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
-							"END)");
+	snprintf(query, sizeof(query), "SELECT slot_name, plugin, two_phase, failover, "
+			 "%s as caught_up, invalidation_reason IS NOT NULL as invalid "
+			 "FROM pg_catalog.pg_replication_slots "
+			 "WHERE slot_type = 'logical' AND "
+			 "database = current_database() AND "
+			 "temporary IS FALSE;",
+			 live_check ? "FALSE" :
+			 "(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
+			 "ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
+			 "END)");
+
+	if (PQsendQuery(conn, query) == 0)
+	{
+		/* TODO: fail */
+	}
+}
+
+static void
+get_old_cluster_logical_slot_infos_result(PGconn *conn, DbInfo *dbinfo)
+{
+	PGresult   *res = PQgetResult(conn);
+	LogicalSlotInfo *slotinfos = NULL;
+	int			num_slots;
+
+	if (PQgetResult(conn) != NULL)
+	{
+		/* TODO: fail */
+	}
 
 	num_slots = PQntuples(res);
 
@@ -720,7 +844,6 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 	}
 
 	PQclear(res);
-	PQfinish(conn);
 
 	dbinfo->slot_arr.slots = slotinfos;
 	dbinfo->slot_arr.nslots = num_slots;
@@ -757,23 +880,36 @@ count_old_cluster_logical_slots(void)
  * not be able to upgrade the logical replication clusters completely.
  */
 static void
-get_db_subscription_count(DbInfo *dbinfo)
+start_db_sub_count_query(PGconn *conn, DbInfo *dbinfo)
 {
-	PGconn	   *conn;
-	PGresult   *res;
+	char		query[QUERY_ALLOC];
 
 	/* Subscriptions can be migrated since PG17. */
 	if (GET_MAJOR_VERSION(old_cluster.major_version) < 1700)
 		return;
 
-	conn = connectToServer(&old_cluster, dbinfo->db_name);
-	res = executeQueryOrDie(conn, "SELECT count(*) "
-							"FROM pg_catalog.pg_subscription WHERE subdbid = %u",
-							dbinfo->db_oid);
+	snprintf(query, sizeof(query), "SELECT count(*) "
+			 "FROM pg_catalog.pg_subscription WHERE subdbid = %u",
+			 dbinfo->db_oid);
+	if (PQsendQuery(conn, query) == 0)
+	{
+		/* TODO: fail */
+	}
+}
+
+static void
+get_db_sub_count_result(PGconn *conn, DbInfo *dbinfo)
+{
+	PGresult   *res = PQgetResult(conn);
+
+	if (PQgetResult(conn) != NULL)
+	{
+		/* TODO: fail */
+	}
+
 	dbinfo->nsubs = atoi(PQgetvalue(res, 0, 0));
 
 	PQclear(res);
-	PQfinish(conn);
 }
 
 /*
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 2b83c340fb..015019b18d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1225,6 +1225,7 @@ IndxInfo
 InferClause
 InferenceElem
 InfoItem
+InfoState
 InhInfo
 InheritableSocket
 InitSampleScan_function
-- 
2.25.1

#2Jeff Davis
pgsql@j-davis.com
In reply to: Nathan Bossart (#1)
Re: optimizing pg_upgrade's once-in-each-database steps

On Thu, 2024-05-16 at 16:16 -0500, Nathan Bossart wrote:

I am posting this early to get thoughts on the general approach.  If
we
proceeded with this strategy, I'd probably create some generic
tooling that
each relevant step would provide a set of callback functions.

The documentation states:

"pg_dump -j uses multiple database connections; it connects to the
database once with the leader process and once again for each worker
job."

That might need to be adjusted.

How much complexity do you avoid by using async instead of multiple
processes?

Also, did you consider connecting once to each database and running
many queries? Most of those seem like just checks.

Regards,
Jeff Davis

#3Nathan Bossart
nathandbossart@gmail.com
In reply to: Jeff Davis (#2)
Re: optimizing pg_upgrade's once-in-each-database steps

On Thu, May 16, 2024 at 05:09:55PM -0700, Jeff Davis wrote:

How much complexity do you avoid by using async instead of multiple
processes?

If we didn't want to use async, my guess is we'd want to use threads to
avoid complicated IPC. And if we followed pgbench's example for using
threads, it might end up at a comparable level of complexity, although I'd
bet that threading would be the more complex of the two. It's hard to say
definitively without coding it up both ways, which might be worth doing.

Also, did you consider connecting once to each database and running
many queries? Most of those seem like just checks.

This was the idea behind 347758b. It may be possible to do more along
these lines. IMO parallelizing will still be useful even if we do combine
more of the steps.

--
Nathan Bossart
Amazon Web Services: https://aws.amazon.com

#4Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#3)
6 attachment(s)
Re: optimizing pg_upgrade's once-in-each-database steps

I figured I'd post what I have so far since this thread hasn't been updated
in a while. The attached patches are still "proof-of-concept grade," but
they are at least moving in the right direction (IMHO). The variable
naming is still not great, and they are woefully undercommented, among
other things.

0001 introduces a new API for registering callbacks and running them in
parallel on all databases in the cluster. This new system manages a set of
"slots" that follow a simple state machine to asynchronously establish a
connection and run the queries. It uses system() to wait for these
asynchronous tasks to complete. Users of this API only need to provide two
callbacks: one to return the query that should be run on each database and
another to process the results of that query. If multiple queries are
required for each database, users can provide multiple sets of callbacks.

The other patches change several of the existing tasks to use this new API.
With these patches applied, I see the following differences in the output
of 'pg_upgrade | ts -i' for a cluster with 1k empty databases:

WITHOUT PATCH

00:00:19 Checking database user is the install user ok
00:00:02 Checking for subscription state ok
00:00:06 Adding ".old" suffix to old global/pg_control ok
00:00:04 Checking for extension updates ok

WITH PATCHES (--jobs 1)

00:00:10 Checking database user is the install user ok
00:00:02 Checking for subscription state ok
00:00:07 Adding ".old" suffix to old global/pg_control ok
00:00:05 Checking for extension updates ok

WITH PATCHES (--jobs 4)

00:00:06 Checking database user is the install user ok
00:00:00 Checking for subscription state ok
00:00:02 Adding ".old" suffix to old global/pg_control ok
00:00:01 Checking for extension updates ok

Note that the "Checking database user is the install user" time also
includes the call to get_db_rel_and_slot_infos() on the old cluster as well
as the call to get_loadable_libraries() on the old cluster. I believe the
improvement with the patches with just one job is due to the consolidation
of the queries into one database connection (presently,
get_db_rel_and_slot_infos() creates 3 connections per database for some
upgrades). Similarly, the "Adding \".old\" suffix to old
global/pg_control" time includes the call to get_db_rel_and_slot_infos() on
the new cluster.

There are several remaining places where we could use this new API to speed
up upgrades. For example, I haven't attempted to use it for the data type
checks yet, and that tends to eat up a sizable chunk of time when there are
many databases.

On Thu, May 16, 2024 at 08:24:08PM -0500, Nathan Bossart wrote:

On Thu, May 16, 2024 at 05:09:55PM -0700, Jeff Davis wrote:

Also, did you consider connecting once to each database and running
many queries? Most of those seem like just checks.

This was the idea behind 347758b. It may be possible to do more along
these lines. IMO parallelizing will still be useful even if we do combine
more of the steps.

My current thinking is that any possible further consolidation should
happen as part of a follow-up effort to parallelization. I'm cautiously
optimistic that the parallelization work will make the consolidation easier
since it moves things to rigidly-defined callback functions.

A separate piece of off-list feedback from Michael Paquier is that this new
parallel system might be something we can teach the ParallelSlot code used
by bin/scripts/ to do. I've yet to look too deeply into this, but I
suspect that it will be difficult to combine the two. For example, the
ParallelSlot system doesn't seem well-suited for the kind of
run-once-in-each-database tasks required by pg_upgrade, and the error
handling is probably little different, too. However, it's still worth a
closer look, and I'm interested in folks' opinions on the subject.

--
nathan

Attachments:

v2-0005-use-new-pg_upgrade-async-API-to-parallelize-getti.patchtext/plain; charset=us-asciiDownload
From 09e7e7baa8c277a3afbed1e2f8d05bfa7fcc586c Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:24:35 -0500
Subject: [PATCH v2 5/6] use new pg_upgrade async API to parallelize getting
 loadable libraries

---
 src/bin/pg_upgrade/function.c | 63 ++++++++++++++++++++---------------
 1 file changed, 37 insertions(+), 26 deletions(-)

diff --git a/src/bin/pg_upgrade/function.c b/src/bin/pg_upgrade/function.c
index 7e3abed098..c11fce0696 100644
--- a/src/bin/pg_upgrade/function.c
+++ b/src/bin/pg_upgrade/function.c
@@ -42,6 +42,32 @@ library_name_compare(const void *p1, const void *p2)
 					  ((const LibraryInfo *) p2)->dbnum);
 }
 
+struct loadable_libraries_state
+{
+	PGresult  **ress;
+	int			totaltups;
+};
+
+static char *
+get_loadable_libraries_query(DbInfo *dbinfo, void *arg)
+{
+	return psprintf("SELECT DISTINCT probin "
+					"FROM pg_catalog.pg_proc "
+					"WHERE prolang = %u AND "
+					"probin IS NOT NULL AND "
+					"oid >= %u;",
+					ClanguageId,
+					FirstNormalObjectId);
+}
+
+static void
+get_loadable_libraries_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct loadable_libraries_state *state = (struct loadable_libraries_state *) arg;
+
+	state->ress[dbinfo - old_cluster.dbarr.dbs] = res;
+	state->totaltups += PQntuples(res);
+}
 
 /*
  * get_loadable_libraries()
@@ -54,47 +80,32 @@ library_name_compare(const void *p1, const void *p2)
 void
 get_loadable_libraries(void)
 {
-	PGresult  **ress;
 	int			totaltups;
 	int			dbnum;
 	int			n_libinfos;
+	AsyncTask  *task = async_task_create();
+	struct loadable_libraries_state state;
 
-	ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
-	totaltups = 0;
+	state.ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
+	state.totaltups = 0;
 
-	/* Fetch all library names, removing duplicates within each DB */
-	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
+	async_task_add_step(task, get_loadable_libraries_query,
+						get_loadable_libraries_result, false, &state);
 
-		/*
-		 * Fetch all libraries containing non-built-in C functions in this DB.
-		 */
-		ress[dbnum] = executeQueryOrDie(conn,
-										"SELECT DISTINCT probin "
-										"FROM pg_catalog.pg_proc "
-										"WHERE prolang = %u AND "
-										"probin IS NOT NULL AND "
-										"oid >= %u;",
-										ClanguageId,
-										FirstNormalObjectId);
-		totaltups += PQntuples(ress[dbnum]);
-
-		PQfinish(conn);
-	}
+	async_task_run(task, &old_cluster);
+	async_task_free(task);
 
 	/*
 	 * Allocate memory for required libraries and logical replication output
 	 * plugins.
 	 */
-	n_libinfos = totaltups + count_old_cluster_logical_slots();
+	n_libinfos = state.totaltups + count_old_cluster_logical_slots();
 	os_info.libraries = (LibraryInfo *) pg_malloc(sizeof(LibraryInfo) * n_libinfos);
 	totaltups = 0;
 
 	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
 	{
-		PGresult   *res = ress[dbnum];
+		PGresult   *res = state.ress[dbnum];
 		int			ntups;
 		int			rowno;
 		LogicalSlotInfoArr *slot_arr = &old_cluster.dbarr.dbs[dbnum].slot_arr;
@@ -129,7 +140,7 @@ get_loadable_libraries(void)
 		}
 	}
 
-	pg_free(ress);
+	pg_free(state.ress);
 
 	os_info.num_libraries = totaltups;
 }
-- 
2.39.3 (Apple Git-146)

v2-0006-use-new-pg_upgrade-async-API-to-parallelize-repor.patchtext/plain; charset=us-asciiDownload
From 7a420ff039d48c54cbb4d06647f039257a807bb9 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:31:57 -0500
Subject: [PATCH v2 6/6] use new pg_upgrade async API to parallelize reporting
 extension updates

---
 src/bin/pg_upgrade/version.c | 82 ++++++++++++++++++------------------
 1 file changed, 41 insertions(+), 41 deletions(-)

diff --git a/src/bin/pg_upgrade/version.c b/src/bin/pg_upgrade/version.c
index 2de6dffccd..12783bb2ba 100644
--- a/src/bin/pg_upgrade/version.c
+++ b/src/bin/pg_upgrade/version.c
@@ -139,6 +139,42 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 		check_ok();
 }
 
+static char *
+report_extension_updates_query(DbInfo *dbinfo, void *arg)
+{
+	return pg_strdup("SELECT name "
+					 "FROM pg_available_extensions "
+					 "WHERE installed_version != default_version");
+}
+
+static void
+report_extension_updates_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_name = PQfnumber(res, "name");
+	char	   *output_path = "update_extensions.sql";
+	FILE	  **script = (FILE **) arg;
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL && (*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			PQExpBufferData connectbuf;
+
+			initPQExpBuffer(&connectbuf);
+			appendPsqlMetaConnect(&connectbuf, dbinfo->db_name);
+			fputs(connectbuf.data, *script);
+			termPQExpBuffer(&connectbuf);
+			db_used = true;
+		}
+		fprintf(*script, "ALTER EXTENSION %s UPDATE;\n",
+				quote_identifier(PQgetvalue(res, rowno, i_name)));
+	}
+}
+
 /*
  * report_extension_updates()
  *	Report extensions that should be updated.
@@ -146,53 +182,17 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 void
 report_extension_updates(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char	   *output_path = "update_extensions.sql";
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for extension updates");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_name;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* find extensions needing updates */
-		res = executeQueryOrDie(conn,
-								"SELECT name "
-								"FROM pg_available_extensions "
-								"WHERE installed_version != default_version"
-			);
-
-		ntups = PQntuples(res);
-		i_name = PQfnumber(res, "name");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				PQExpBufferData connectbuf;
-
-				initPQExpBuffer(&connectbuf);
-				appendPsqlMetaConnect(&connectbuf, active_db->db_name);
-				fputs(connectbuf.data, script);
-				termPQExpBuffer(&connectbuf);
-				db_used = true;
-			}
-			fprintf(script, "ALTER EXTENSION %s UPDATE;\n",
-					quote_identifier(PQgetvalue(res, rowno, i_name)));
-		}
+	async_task_add_step(task, report_extension_updates_query,
+						report_extension_updates_result, true, &script);
 
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v2-0004-use-new-pg_upgrade-async-API-for-retrieving-relin.patchtext/plain; charset=us-asciiDownload
From 48943ad85f83ba44ea01e4b1fdd5c4afc53552e3 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:09:33 -0500
Subject: [PATCH v2 4/6] use new pg_upgrade async API for retrieving relinfos

---
 src/bin/pg_upgrade/info.c | 187 +++++++++++++++++---------------------
 1 file changed, 81 insertions(+), 106 deletions(-)

diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 8f1777de59..d07255bd0a 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -22,13 +22,16 @@ static void report_unmatched_relation(const RelInfo *rel, const DbInfo *db,
 static void free_db_and_rel_infos(DbInfoArr *db_arr);
 static void get_template0_info(ClusterInfo *cluster);
 static void get_db_infos(ClusterInfo *cluster);
-static void get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo);
+static char *get_rel_infos_query(DbInfo *dbinfo, void *arg);
+static void get_rel_infos_result(DbInfo *dbinfo, PGresult *res, void *arg);
 static void free_rel_infos(RelInfoArr *rel_arr);
 static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
-static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo);
-static void get_db_subscription_count(DbInfo *dbinfo);
+static char *get_old_cluster_logical_slot_infos_query(DbInfo *dbinfo, void *arg);
+static void get_old_cluster_logical_slot_infos_result(DbInfo *dbinfo, PGresult *res, void *arg);
+static char *get_db_subscription_count_query(DbInfo *dbinfo, void *arg);
+static void get_db_subscription_count_result(DbInfo *dbinfo, PGresult *res, void *arg);
 
 
 /*
@@ -277,7 +280,7 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
 void
 get_db_rel_and_slot_infos(ClusterInfo *cluster)
 {
-	int			dbnum;
+	AsyncTask  *task = async_task_create();
 
 	if (cluster->dbarr.dbs != NULL)
 		free_db_and_rel_infos(&cluster->dbarr);
@@ -285,23 +288,26 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster)
 	get_template0_info(cluster);
 	get_db_infos(cluster);
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	async_task_add_step(task,
+						get_rel_infos_query,
+						get_rel_infos_result,
+						true, NULL);
+	if (cluster == &old_cluster &&
+		GET_MAJOR_VERSION(cluster->major_version) > 1600)
 	{
-		DbInfo	   *pDbInfo = &cluster->dbarr.dbs[dbnum];
-
-		get_rel_infos(cluster, pDbInfo);
-
-		/*
-		 * Retrieve the logical replication slots infos and the subscriptions
-		 * count for the old cluster.
-		 */
-		if (cluster == &old_cluster)
-		{
-			get_old_cluster_logical_slot_infos(pDbInfo);
-			get_db_subscription_count(pDbInfo);
-		}
+		async_task_add_step(task,
+							get_old_cluster_logical_slot_infos_query,
+							get_old_cluster_logical_slot_infos_result,
+							true, cluster);
+		async_task_add_step(task,
+							get_db_subscription_count_query,
+							get_db_subscription_count_result,
+							true, cluster);
 	}
 
+	async_task_run(task, cluster);
+	async_task_free(task);
+
 	if (cluster == &old_cluster)
 		pg_log(PG_VERBOSE, "\nsource databases:");
 	else
@@ -447,30 +453,10 @@ get_db_infos(ClusterInfo *cluster)
  * Note: the resulting RelInfo array is assumed to be sorted by OID.
  * This allows later processing to match up old and new databases efficiently.
  */
-static void
-get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
+static char *
+get_rel_infos_query(DbInfo *dbinfo, void *arg)
 {
-	PGconn	   *conn = connectToServer(cluster,
-									   dbinfo->db_name);
-	PGresult   *res;
-	RelInfo    *relinfos;
-	int			ntups;
-	int			relnum;
-	int			num_rels = 0;
-	char	   *nspname = NULL;
-	char	   *relname = NULL;
-	char	   *tablespace = NULL;
-	int			i_spclocation,
-				i_nspname,
-				i_relname,
-				i_reloid,
-				i_indtable,
-				i_toastheap,
-				i_relfilenumber,
-				i_reltablespace;
-	char		query[QUERY_ALLOC];
-	char	   *last_namespace = NULL,
-			   *last_tablespace = NULL;
+	char	   *query = pg_malloc(QUERY_ALLOC);
 
 	query[0] = '\0';			/* initialize query string to empty */
 
@@ -484,7 +470,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * output, so we have to copy that system table.  It's easiest to do that
 	 * by treating it as a user table.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
+	snprintf(query + strlen(query), QUERY_ALLOC - strlen(query),
 			 "WITH regular_heap (reloid, indtable, toastheap) AS ( "
 			 "  SELECT c.oid, 0::oid, 0::oid "
 			 "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
@@ -506,7 +492,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * selected by the regular_heap CTE.  (We have to do this separately
 	 * because the namespace-name rules above don't work for toast tables.)
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
+	snprintf(query + strlen(query), QUERY_ALLOC - strlen(query),
 			 "  toast_heap (reloid, indtable, toastheap) AS ( "
 			 "  SELECT c.reltoastrelid, 0::oid, c.oid "
 			 "  FROM regular_heap JOIN pg_catalog.pg_class c "
@@ -519,7 +505,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * Testing indisready is necessary in 9.2, and harmless in earlier/later
 	 * versions.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
+	snprintf(query + strlen(query), QUERY_ALLOC - strlen(query),
 			 "  all_index (reloid, indtable, toastheap) AS ( "
 			 "  SELECT indexrelid, indrelid, 0::oid "
 			 "  FROM pg_catalog.pg_index "
@@ -533,7 +519,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * And now we can write the query that retrieves the data we want for each
 	 * heap and index relation.  Make sure result is sorted by OID.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
+	snprintf(query + strlen(query), QUERY_ALLOC - strlen(query),
 			 "SELECT all_rels.*, n.nspname, c.relname, "
 			 "  c.relfilenode, c.reltablespace, "
 			 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
@@ -550,22 +536,30 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 			 "     ON c.reltablespace = t.oid "
 			 "ORDER BY 1;");
 
-	res = executeQueryOrDie(conn, "%s", query);
-
-	ntups = PQntuples(res);
-
-	relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	return query;
+}
 
-	i_reloid = PQfnumber(res, "reloid");
-	i_indtable = PQfnumber(res, "indtable");
-	i_toastheap = PQfnumber(res, "toastheap");
-	i_nspname = PQfnumber(res, "nspname");
-	i_relname = PQfnumber(res, "relname");
-	i_relfilenumber = PQfnumber(res, "relfilenode");
-	i_reltablespace = PQfnumber(res, "reltablespace");
-	i_spclocation = PQfnumber(res, "spclocation");
+static void
+get_rel_infos_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	int			ntups = PQntuples(res);
+	RelInfo    *relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	int			i_reloid = PQfnumber(res, "reloid");
+	int			i_indtable = PQfnumber(res, "indtable");
+	int			i_toastheap = PQfnumber(res, "toastheap");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+	int			i_relfilenumber = PQfnumber(res, "relfilenode");
+	int			i_reltablespace = PQfnumber(res, "reltablespace");
+	int			i_spclocation = PQfnumber(res, "spclocation");
+	int			num_rels = 0;
+	char	   *nspname = NULL;
+	char	   *relname = NULL;
+	char	   *tablespace = NULL;
+	char	   *last_namespace = NULL;
+	char	   *last_tablespace = NULL;
 
-	for (relnum = 0; relnum < ntups; relnum++)
+	for (int relnum = 0; relnum < ntups; relnum++)
 	{
 		RelInfo    *curr = &relinfos[num_rels++];
 
@@ -618,9 +612,6 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 			/* A zero reltablespace oid indicates the database tablespace. */
 			curr->tablespace = dbinfo->db_tablespace;
 	}
-	PQclear(res);
-
-	PQfinish(conn);
 
 	dbinfo->rel_arr.rels = relinfos;
 	dbinfo->rel_arr.nrels = num_rels;
@@ -642,20 +633,9 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
  * check_old_cluster_for_valid_slots() would raise a FATAL error if such slots
  * are included.
  */
-static void
-get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
+static char *
+get_old_cluster_logical_slot_infos_query(DbInfo *dbinfo, void *arg)
 {
-	PGconn	   *conn;
-	PGresult   *res;
-	LogicalSlotInfo *slotinfos = NULL;
-	int			num_slots;
-
-	/* Logical slots can be migrated since PG17. */
-	if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1600)
-		return;
-
-	conn = connectToServer(&old_cluster, dbinfo->db_name);
-
 	/*
 	 * Fetch the logical replication slot information. The check whether the
 	 * slot is considered caught up is done by an upgrade function. This
@@ -673,18 +653,23 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
-							"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
-							"FROM pg_catalog.pg_replication_slots "
-							"WHERE slot_type = 'logical' AND "
-							"database = current_database() AND "
-							"temporary IS FALSE;",
-							user_opts.live_check ? "FALSE" :
-							"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
-							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
-							"END)");
-
-	num_slots = PQntuples(res);
+	return psprintf("SELECT slot_name, plugin, two_phase, failover, "
+					"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
+					"FROM pg_catalog.pg_replication_slots "
+					"WHERE slot_type = 'logical' AND "
+					"database = current_database() AND "
+					"temporary IS FALSE;",
+					user_opts.live_check ? "FALSE" :
+					"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
+					"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
+					"END)");
+}
+
+static void
+get_old_cluster_logical_slot_infos_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	LogicalSlotInfo *slotinfos = NULL;
+	int			num_slots = PQntuples(res);
 
 	if (num_slots)
 	{
@@ -717,14 +702,10 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 		}
 	}
 
-	PQclear(res);
-	PQfinish(conn);
-
 	dbinfo->slot_arr.slots = slotinfos;
 	dbinfo->slot_arr.nslots = num_slots;
 }
 
-
 /*
  * count_old_cluster_logical_slots()
  *
@@ -754,24 +735,18 @@ count_old_cluster_logical_slots(void)
  * This is because before that the logical slots are not upgraded, so we will
  * not be able to upgrade the logical replication clusters completely.
  */
-static void
-get_db_subscription_count(DbInfo *dbinfo)
+static char *
+get_db_subscription_count_query(DbInfo *dbinfo, void *arg)
 {
-	PGconn	   *conn;
-	PGresult   *res;
-
-	/* Subscriptions can be migrated since PG17. */
-	if (GET_MAJOR_VERSION(old_cluster.major_version) < 1700)
-		return;
+	return psprintf("SELECT count(*) "
+					"FROM pg_catalog.pg_subscription WHERE subdbid = %u",
+					dbinfo->db_oid);
+}
 
-	conn = connectToServer(&old_cluster, dbinfo->db_name);
-	res = executeQueryOrDie(conn, "SELECT count(*) "
-							"FROM pg_catalog.pg_subscription WHERE subdbid = %u",
-							dbinfo->db_oid);
+static void
+get_db_subscription_count_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
 	dbinfo->nsubs = atoi(PQgetvalue(res, 0, 0));
-
-	PQclear(res);
-	PQfinish(conn);
 }
 
 /*
-- 
2.39.3 (Apple Git-146)

v2-0001-introduce-framework-for-parallelizing-pg_upgrade-.patchtext/plain; charset=us-asciiDownload
From d7683a095d4d2c1574005eb41504a5be256d6480 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 11:02:44 -0500
Subject: [PATCH v2 1/6] introduce framework for parallelizing pg_upgrade tasks

---
 src/bin/pg_upgrade/Makefile      |   1 +
 src/bin/pg_upgrade/async.c       | 323 +++++++++++++++++++++++++++++++
 src/bin/pg_upgrade/meson.build   |   1 +
 src/bin/pg_upgrade/pg_upgrade.h  |  16 ++
 src/tools/pgindent/typedefs.list |   4 +
 5 files changed, 345 insertions(+)
 create mode 100644 src/bin/pg_upgrade/async.c

diff --git a/src/bin/pg_upgrade/Makefile b/src/bin/pg_upgrade/Makefile
index bde91e2beb..3bc4f5d740 100644
--- a/src/bin/pg_upgrade/Makefile
+++ b/src/bin/pg_upgrade/Makefile
@@ -12,6 +12,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	$(WIN32RES) \
+	async.o \
 	check.o \
 	controldata.o \
 	dump.o \
diff --git a/src/bin/pg_upgrade/async.c b/src/bin/pg_upgrade/async.c
new file mode 100644
index 0000000000..7df1e7712d
--- /dev/null
+++ b/src/bin/pg_upgrade/async.c
@@ -0,0 +1,323 @@
+/*
+ * async.c
+ *
+ * parallelization via libpq's async APIs
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ * src/bin/pg_upgrade/async.c
+ */
+
+#include "postgres_fe.h"
+
+#include "common/connect.h"
+#include "fe_utils/string_utils.h"
+#include "pg_upgrade.h"
+
+static int	dbs_complete;
+static int	dbs_processing;
+
+typedef struct AsyncTaskCallbacks
+{
+	AsyncTaskGetQueryCB query_cb;
+	AsyncTaskProcessCB process_cb;
+	bool		free_result;
+	void	   *arg;
+} AsyncTaskCallbacks;
+
+typedef struct AsyncTask
+{
+	AsyncTaskCallbacks *cbs;
+	int			num_cb_sets;
+} AsyncTask;
+
+typedef enum
+{
+	FREE,
+	CONNECTING,
+	SETTING_SEARCH_PATH,
+	RUNNING_QUERY,
+} AsyncSlotState;
+
+typedef struct
+{
+	AsyncSlotState state;
+	int			db;
+	int			query;
+	PGconn	   *conn;
+} AsyncSlot;
+
+AsyncTask *
+async_task_create(void)
+{
+	return pg_malloc0(sizeof(AsyncTask));
+}
+
+void
+async_task_free(AsyncTask *task)
+{
+	if (task->cbs)
+		pg_free(task->cbs);
+
+	pg_free(task);
+}
+
+void
+async_task_add_step(AsyncTask *task,
+					AsyncTaskGetQueryCB query_cb,
+					AsyncTaskProcessCB process_cb, bool free_result,
+					void *arg)
+{
+	AsyncTaskCallbacks *new_cbs;
+
+	task->cbs = pg_realloc(task->cbs,
+						   ++task->num_cb_sets * sizeof(AsyncTaskCallbacks));
+
+	new_cbs = &task->cbs[task->num_cb_sets - 1];
+	new_cbs->query_cb = query_cb;
+	new_cbs->process_cb = process_cb;
+	new_cbs->free_result = free_result;
+	new_cbs->arg = arg;
+}
+
+static void
+conn_failure(PGconn *conn)
+{
+	pg_log(PG_REPORT, "%s", PQerrorMessage(conn));
+	printf(_("Failure, exiting\n"));
+	exit(1);
+}
+
+static void
+start_conn(const ClusterInfo *cluster, AsyncSlot *slot)
+{
+	PQExpBufferData conn_opts;
+
+	/* Build connection string with proper quoting */
+	initPQExpBuffer(&conn_opts);
+	appendPQExpBufferStr(&conn_opts, "dbname=");
+	appendConnStrVal(&conn_opts, cluster->dbarr.dbs[slot->db].db_name);
+	appendPQExpBufferStr(&conn_opts, " user=");
+	appendConnStrVal(&conn_opts, os_info.user);
+	appendPQExpBuffer(&conn_opts, " port=%d", cluster->port);
+	if (cluster->sockdir)
+	{
+		appendPQExpBufferStr(&conn_opts, " host=");
+		appendConnStrVal(&conn_opts, cluster->sockdir);
+	}
+
+	slot->conn = PQconnectStart(conn_opts.data);
+	termPQExpBuffer(&conn_opts);
+
+	if (!slot->conn)
+		conn_failure(slot->conn);
+}
+
+static void
+dispatch_query(const ClusterInfo *cluster, AsyncSlot *slot,
+			   const AsyncTask *task)
+{
+	AsyncTaskCallbacks *cbs = &task->cbs[slot->query];
+	AsyncTaskGetQueryCB get_query = cbs->query_cb;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db];
+	char	   *query = (*get_query) (dbinfo, cbs->arg);
+
+	if (!PQsendQuery(slot->conn, query))
+		conn_failure(slot->conn);
+
+	pg_free(query);
+}
+
+static PGresult *
+get_last_result(PGconn *conn)
+{
+	PGresult   *tmp;
+	PGresult   *res = NULL;
+
+	while ((tmp = PQgetResult(conn)) != NULL)
+	{
+		PQclear(res);
+		res = tmp;
+		if (PQstatus(conn) == CONNECTION_BAD)
+			conn_failure(conn);
+	}
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK &&
+		PQresultStatus(res) != PGRES_TUPLES_OK)
+		conn_failure(conn);
+
+	return res;
+}
+
+static void
+process_query_result(const ClusterInfo *cluster, AsyncSlot *slot,
+					 const AsyncTask *task)
+{
+	AsyncTaskCallbacks *cbs = &task->cbs[slot->query];
+	AsyncTaskProcessCB process_cb = cbs->process_cb;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db];
+	PGresult   *res = get_last_result(slot->conn);
+
+	(*process_cb) (dbinfo, res, cbs->arg);
+
+	if (cbs->free_result)
+		PQclear(res);
+}
+
+static void
+process_slot(const ClusterInfo *cluster, AsyncSlot *slot, const AsyncTask *task)
+{
+	switch (slot->state)
+	{
+		case FREE:
+			if (dbs_processing >= cluster->dbarr.ndbs)
+				return;
+			slot->db = dbs_processing++;
+			slot->state = CONNECTING;
+			start_conn(cluster, slot);
+			return;
+
+		case CONNECTING:
+			if (PQconnectPoll(slot->conn) == PGRES_POLLING_FAILED)
+				conn_failure(slot->conn);
+			if (PQconnectPoll(slot->conn) != PGRES_POLLING_OK)
+				return;
+			slot->state = SETTING_SEARCH_PATH;
+			if (!PQsendQuery(slot->conn, ALWAYS_SECURE_SEARCH_PATH_SQL))
+				conn_failure(slot->conn);
+			return;
+
+		case SETTING_SEARCH_PATH:
+			if (!PQconsumeInput(slot->conn))
+				conn_failure(slot->conn);
+			if (PQisBusy(slot->conn))
+				return;
+			PQclear(get_last_result(slot->conn));
+			slot->state = RUNNING_QUERY;
+			dispatch_query(cluster, slot, task);
+			return;
+
+		case RUNNING_QUERY:
+			if (!PQconsumeInput(slot->conn))
+				conn_failure(slot->conn);
+			if (PQisBusy(slot->conn))
+				return;
+			process_query_result(cluster, slot, task);
+			if (++slot->query >= task->num_cb_sets)
+			{
+				dbs_complete++;
+				PQfinish(slot->conn);
+				memset(slot, 0, sizeof(AsyncSlot));
+				return;
+			}
+			dispatch_query(cluster, slot, task);
+			return;
+	}
+}
+
+/*
+ * Wait on the slots to either finish connecting or to receive query results if
+ * possible.  This avoids a tight loop in async_task_run().
+ */
+static void
+wait_on_slots(AsyncSlot *slots, int numslots)
+{
+	fd_set		input_mask;
+	fd_set		output_mask;
+	fd_set		except_mask;
+	int			maxFd = 0;
+
+	FD_ZERO(&input_mask);
+	FD_ZERO(&output_mask);
+	FD_ZERO(&except_mask);
+
+	for (int i = 0; i < numslots; i++)
+	{
+		int			sock;
+		bool		read = false;
+
+		switch (slots[i].state)
+		{
+			case FREE:
+
+				/*
+				 * If we see a free slot, return right away so that it can be
+				 * reused immediately for the next database.  This might cause
+				 * us to spin more than necessary as we finish processing the
+				 * last few databases, but that shouldn't cause too much harm.
+				 */
+				return;
+
+			case CONNECTING:
+
+				/*
+				 * If we are waiting for the connection to establish, choose
+				 * whether to wait for reading or for writing on the socket as
+				 * appropriate.  If neither apply, just return immediately so
+				 * that we can handle the slot.
+				 */
+				{
+					PostgresPollingStatusType status;
+
+					status = PQconnectPoll(slots[i].conn);
+					if (status == PGRES_POLLING_READING)
+						read = true;
+					else if (status != PGRES_POLLING_WRITING)
+						return;
+				}
+				break;
+
+			case SETTING_SEARCH_PATH:
+			case RUNNING_QUERY:
+
+				/*
+				 * If we've sent a query, we must wait for the socket to be
+				 * read-ready.  Note that process_slot() handles calling
+				 * PQconsumeInput() as required.
+				 */
+				read = true;
+				break;
+		}
+
+		/*
+		 * If there's some problem retrieving the socket, just pretend this
+		 * slot doesn't exist.  We don't expect this to happen regularly in
+		 * practice, so it seems unlikely to cause too much harm.
+		 */
+		sock = PQsocket(slots[i].conn);
+		if (sock < 0)
+			continue;
+
+		/*
+		 * Add the socket to the set.
+		 */
+		FD_SET(sock, read ? &input_mask : &output_mask);
+		FD_SET(sock, &except_mask);
+		maxFd = Max(maxFd, sock);
+	}
+
+	/*
+	 * If we found socket(s) to wait on, wait.
+	 */
+	if (maxFd != 0)
+		(void) select(maxFd + 1, &input_mask, &output_mask, &except_mask, NULL);
+}
+
+void
+async_task_run(const AsyncTask *task, const ClusterInfo *cluster)
+{
+	int			jobs = Max(1, user_opts.jobs);
+	AsyncSlot  *slots = pg_malloc0(sizeof(AsyncSlot) * jobs);
+
+	dbs_complete = 0;
+	dbs_processing = 0;
+
+	while (dbs_complete < cluster->dbarr.ndbs)
+	{
+		for (int i = 0; i < jobs; i++)
+			process_slot(cluster, &slots[i], task);
+
+		wait_on_slots(slots, jobs);
+	}
+
+	pg_free(slots);
+}
diff --git a/src/bin/pg_upgrade/meson.build b/src/bin/pg_upgrade/meson.build
index 9825fa3305..9eb48e176c 100644
--- a/src/bin/pg_upgrade/meson.build
+++ b/src/bin/pg_upgrade/meson.build
@@ -1,6 +1,7 @@
 # Copyright (c) 2022-2024, PostgreSQL Global Development Group
 
 pg_upgrade_sources = files(
+  'async.c',
   'check.c',
   'controldata.c',
   'dump.c',
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 8afe240bdf..1ebad3bd74 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -494,3 +494,19 @@ void		parallel_transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr
 										  char *old_pgdata, char *new_pgdata,
 										  char *old_tablespace);
 bool		reap_child(bool wait_for_child);
+
+/* async.c */
+
+typedef char *(*AsyncTaskGetQueryCB) (DbInfo *dbinfo, void *arg);
+typedef void (*AsyncTaskProcessCB) (DbInfo *dbinfo, PGresult *res, void *arg);
+
+/* struct definition is private to async.c */
+typedef struct AsyncTask AsyncTask;
+
+AsyncTask  *async_task_create(void);
+void		async_task_add_step(AsyncTask *task,
+								AsyncTaskGetQueryCB query_cb,
+								AsyncTaskProcessCB process_cb, bool free_result,
+								void *arg);
+void		async_task_run(const AsyncTask *task, const ClusterInfo *cluster);
+void		async_task_free(AsyncTask *task);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e6c1caf649..3d219cbfe2 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -153,6 +153,10 @@ ArrayMetaState
 ArraySubWorkspace
 ArrayToken
 ArrayType
+AsyncSlot
+AsyncSlotState
+AsyncTask
+AsyncTaskCallbacks
 AsyncQueueControl
 AsyncQueueEntry
 AsyncRequest
-- 
2.39.3 (Apple Git-146)

v2-0002-use-new-pg_upgrade-async-API-for-subscription-sta.patchtext/plain; charset=us-asciiDownload
From c84b3c97cb0befff8027702f1674e809f174b3aa Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 17:21:19 -0500
Subject: [PATCH v2 2/6] use new pg_upgrade async API for subscription state
 checks

---
 src/bin/pg_upgrade/check.c | 200 ++++++++++++++++++++-----------------
 1 file changed, 106 insertions(+), 94 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 27924159d6..f653fa25a5 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1906,6 +1906,75 @@ check_old_cluster_for_valid_slots(bool live_check)
 	check_ok();
 }
 
+/* private state for subscription state checks */
+struct substate_info
+{
+	FILE	   *script;
+	char		output_path[MAXPGPATH];
+};
+
+/*
+ * We don't allow upgrade if there is a risk of dangling slot or origin
+ * corresponding to initial sync after upgrade.
+ *
+ * A slot/origin not created yet refers to the 'i' (initialize) state, while
+ * 'r' (ready) state refers to a slot/origin created previously but already
+ * dropped. These states are supported for pg_upgrade. The other states listed
+ * below are not supported:
+ *
+ * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state would
+ * retain a replication slot, which could not be dropped by the sync worker
+ * spawned after the upgrade because the subscription ID used for the slot name
+ * won't match anymore.
+ *
+ * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state would
+ * retain the replication origin when there is a failure in tablesync worker
+ * immediately after dropping the replication slot in the publisher.
+ *
+ * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on a
+ * relation upgraded while in this state would expect an origin ID with the OID
+ * of the subscription used before the upgrade, causing it to fail.
+ *
+ * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and SUBREL_STATE_UNKNOWN:
+ * These states are not stored in the catalog, so we need not allow these
+ * states.
+ */
+static char *
+sub_query(DbInfo *dbinfo, void *arg)
+{
+	return pg_strdup("SELECT r.srsubstate, s.subname, n.nspname, c.relname "
+					 "FROM pg_catalog.pg_subscription_rel r "
+					 "LEFT JOIN pg_catalog.pg_subscription s"
+					 "   ON r.srsubid = s.oid "
+					 "LEFT JOIN pg_catalog.pg_class c"
+					 "   ON r.srrelid = c.oid "
+					 "LEFT JOIN pg_catalog.pg_namespace n"
+					 "   ON c.relnamespace = n.oid "
+					 "WHERE r.srsubstate NOT IN ('i', 'r') "
+					 "ORDER BY s.subname");
+}
+
+static void
+sub_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct substate_info *state = (struct substate_info *) arg;
+	int			ntup = PQntuples(res);
+
+	for (int i = 0; i < ntup; i++)
+	{
+		if (state->script == NULL &&
+			(state->script = fopen_priv(state->output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state->output_path);
+
+		fprintf(state->script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
+				PQgetvalue(res, i, 0),
+				dbinfo->db_name,
+				PQgetvalue(res, i, 1),
+				PQgetvalue(res, i, 2),
+				PQgetvalue(res, i, 3));
+	}
+}
+
 /*
  * check_old_cluster_subscription_state()
  *
@@ -1916,115 +1985,58 @@ check_old_cluster_for_valid_slots(bool live_check)
 static void
 check_old_cluster_subscription_state(void)
 {
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
+	struct substate_info state;
+	PGresult   *res;
+	PGconn	   *conn;
 	int			ntup;
 
 	prep_status("Checking for subscription state");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	state.script = NULL;
+	snprintf(state.output_path, sizeof(state.output_path), "%s/%s",
 			 log_opts.basedir,
 			 "subs_invalid.txt");
-	for (int dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
 
-		/* We need to check for pg_replication_origin only once. */
-		if (dbnum == 0)
-		{
-			/*
-			 * Check that all the subscriptions have their respective
-			 * replication origin.
-			 */
-			res = executeQueryOrDie(conn,
-									"SELECT d.datname, s.subname "
-									"FROM pg_catalog.pg_subscription s "
-									"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
-									"	ON o.roname = 'pg_' || s.oid "
-									"INNER JOIN pg_catalog.pg_database d "
-									"	ON d.oid = s.subdbid "
-									"WHERE o.roname IS NULL;");
-
-			ntup = PQntuples(res);
-			for (int i = 0; i < ntup; i++)
-			{
-				if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-					pg_fatal("could not open file \"%s\": %m", output_path);
-				fprintf(script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
-						PQgetvalue(res, i, 0),
-						PQgetvalue(res, i, 1));
-			}
-			PQclear(res);
-		}
-
-		/*
-		 * We don't allow upgrade if there is a risk of dangling slot or
-		 * origin corresponding to initial sync after upgrade.
-		 *
-		 * A slot/origin not created yet refers to the 'i' (initialize) state,
-		 * while 'r' (ready) state refers to a slot/origin created previously
-		 * but already dropped. These states are supported for pg_upgrade. The
-		 * other states listed below are not supported:
-		 *
-		 * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state
-		 * would retain a replication slot, which could not be dropped by the
-		 * sync worker spawned after the upgrade because the subscription ID
-		 * used for the slot name won't match anymore.
-		 *
-		 * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state
-		 * would retain the replication origin when there is a failure in
-		 * tablesync worker immediately after dropping the replication slot in
-		 * the publisher.
-		 *
-		 * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on
-		 * a relation upgraded while in this state would expect an origin ID
-		 * with the OID of the subscription used before the upgrade, causing
-		 * it to fail.
-		 *
-		 * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and
-		 * SUBREL_STATE_UNKNOWN: These states are not stored in the catalog,
-		 * so we need not allow these states.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT r.srsubstate, s.subname, n.nspname, c.relname "
-								"FROM pg_catalog.pg_subscription_rel r "
-								"LEFT JOIN pg_catalog.pg_subscription s"
-								"	ON r.srsubid = s.oid "
-								"LEFT JOIN pg_catalog.pg_class c"
-								"	ON r.srrelid = c.oid "
-								"LEFT JOIN pg_catalog.pg_namespace n"
-								"	ON c.relnamespace = n.oid "
-								"WHERE r.srsubstate NOT IN ('i', 'r') "
-								"ORDER BY s.subname");
-
-		ntup = PQntuples(res);
-		for (int i = 0; i < ntup; i++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
+	/*
+	 * Check that all the subscriptions have their respective replication
+	 * origin.  This check only needs to run once.
+	 */
+	conn = connectToServer(&old_cluster, old_cluster.dbarr.dbs[0].db_name);
+	res = executeQueryOrDie(conn,
+							"SELECT d.datname, s.subname "
+							"FROM pg_catalog.pg_subscription s "
+							"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
+							"	ON o.roname = 'pg_' || s.oid "
+							"INNER JOIN pg_catalog.pg_database d "
+							"	ON d.oid = s.subdbid "
+							"WHERE o.roname IS NULL;");
+	ntup = PQntuples(res);
+	for (int i = 0; i < ntup; i++)
+	{
+		if (state.script == NULL &&
+			(state.script = fopen_priv(state.output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state.output_path);
+		fprintf(state.script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
+				PQgetvalue(res, i, 0),
+				PQgetvalue(res, i, 1));
+	}
+	PQclear(res);
+	PQfinish(conn);
 
-			fprintf(script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
-					PQgetvalue(res, i, 0),
-					active_db->db_name,
-					PQgetvalue(res, i, 1),
-					PQgetvalue(res, i, 2),
-					PQgetvalue(res, i, 3));
-		}
+	async_task_add_step(task, sub_query, sub_process, true, &state);
 
-		PQclear(res);
-		PQfinish(conn);
-	}
+	async_task_run(task, &old_cluster);
+	async_task_free(task);
 
-	if (script)
+	if (state.script)
 	{
-		fclose(script);
+		fclose(state.script);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains subscriptions without origin or having relations not in i (initialize) or r (ready) state.\n"
 				 "You can allow the initial sync to finish for all relations and then restart the upgrade.\n"
 				 "A list of the problematic subscriptions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", state.output_path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v2-0003-move-live_check-variable-to-user_opts.patchtext/plain; charset=us-asciiDownload
From c9d2c483ac1fabc0897c19a60a1cf6054e1293da Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 21 May 2024 16:35:19 -0500
Subject: [PATCH v2 3/6] move live_check variable to user_opts

---
 src/bin/pg_upgrade/check.c       | 32 ++++++++++++++++----------------
 src/bin/pg_upgrade/controldata.c |  5 +++--
 src/bin/pg_upgrade/info.c        | 12 +++++-------
 src/bin/pg_upgrade/option.c      |  4 ++--
 src/bin/pg_upgrade/pg_upgrade.c  | 21 ++++++++++-----------
 src/bin/pg_upgrade/pg_upgrade.h  | 13 +++++++------
 6 files changed, 43 insertions(+), 44 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index f653fa25a5..251f3d9017 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -29,7 +29,7 @@ static void check_for_new_tablespace_dir(void);
 static void check_for_user_defined_encoding_conversions(ClusterInfo *cluster);
 static void check_new_cluster_logical_replication_slots(void);
 static void check_new_cluster_subscription_configuration(void);
-static void check_old_cluster_for_valid_slots(bool live_check);
+static void check_old_cluster_for_valid_slots(void);
 static void check_old_cluster_subscription_state(void);
 
 /*
@@ -555,9 +555,9 @@ fix_path_separator(char *path)
 }
 
 void
-output_check_banner(bool live_check)
+output_check_banner(void)
 {
-	if (user_opts.check && live_check)
+	if (user_opts.live_check)
 	{
 		pg_log(PG_REPORT,
 			   "Performing Consistency Checks on Old Live Server\n"
@@ -573,18 +573,18 @@ output_check_banner(bool live_check)
 
 
 void
-check_and_dump_old_cluster(bool live_check)
+check_and_dump_old_cluster(void)
 {
 	/* -- OLD -- */
 
-	if (!live_check)
+	if (!user_opts.live_check)
 		start_postmaster(&old_cluster, true);
 
 	/*
 	 * Extract a list of databases, tables, and logical replication slots from
 	 * the old cluster.
 	 */
-	get_db_rel_and_slot_infos(&old_cluster, live_check);
+	get_db_rel_and_slot_infos(&old_cluster);
 
 	init_tablespaces();
 
@@ -605,7 +605,7 @@ check_and_dump_old_cluster(bool live_check)
 		 * Logical replication slots can be migrated since PG17. See comments
 		 * atop get_old_cluster_logical_slot_infos().
 		 */
-		check_old_cluster_for_valid_slots(live_check);
+		check_old_cluster_for_valid_slots();
 
 		/*
 		 * Subscriptions and their dependencies can be migrated since PG17.
@@ -652,7 +652,7 @@ check_and_dump_old_cluster(bool live_check)
 	 */
 	if (GET_MAJOR_VERSION(old_cluster.major_version) <= 906)
 	{
-		if (user_opts.check)
+		if (user_opts.live_check)
 			old_9_6_invalidate_hash_indexes(&old_cluster, true);
 	}
 
@@ -667,7 +667,7 @@ check_and_dump_old_cluster(bool live_check)
 	if (!user_opts.check)
 		generate_old_dump();
 
-	if (!live_check)
+	if (!user_opts.live_check)
 		stop_postmaster(false);
 }
 
@@ -675,7 +675,7 @@ check_and_dump_old_cluster(bool live_check)
 void
 check_new_cluster(void)
 {
-	get_db_rel_and_slot_infos(&new_cluster, false);
+	get_db_rel_and_slot_infos(&new_cluster);
 
 	check_new_cluster_is_empty();
 
@@ -826,14 +826,14 @@ check_cluster_versions(void)
 
 
 void
-check_cluster_compatibility(bool live_check)
+check_cluster_compatibility(void)
 {
 	/* get/check pg_control data of servers */
-	get_control_data(&old_cluster, live_check);
-	get_control_data(&new_cluster, false);
+	get_control_data(&old_cluster);
+	get_control_data(&new_cluster);
 	check_control_data(&old_cluster.controldata, &new_cluster.controldata);
 
-	if (live_check && old_cluster.port == new_cluster.port)
+	if (user_opts.live_check && old_cluster.port == new_cluster.port)
 		pg_fatal("When checking a live server, "
 				 "the old and new port numbers must be different.");
 }
@@ -1839,7 +1839,7 @@ check_new_cluster_subscription_configuration(void)
  * before shutdown.
  */
 static void
-check_old_cluster_for_valid_slots(bool live_check)
+check_old_cluster_for_valid_slots(void)
 {
 	char		output_path[MAXPGPATH];
 	FILE	   *script = NULL;
@@ -1878,7 +1878,7 @@ check_old_cluster_for_valid_slots(bool live_check)
 			 * Note: This can be satisfied only when the old cluster has been
 			 * shut down, so we skip this for live checks.
 			 */
-			if (!live_check && !slot->caught_up)
+			if (!user_opts.live_check && !slot->caught_up)
 			{
 				if (script == NULL &&
 					(script = fopen_priv(output_path, "w")) == NULL)
diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c
index 1f0ccea3ed..cf665b9dee 100644
--- a/src/bin/pg_upgrade/controldata.c
+++ b/src/bin/pg_upgrade/controldata.c
@@ -33,7 +33,7 @@
  * return valid xid data for a running server.
  */
 void
-get_control_data(ClusterInfo *cluster, bool live_check)
+get_control_data(ClusterInfo *cluster)
 {
 	char		cmd[MAXPGPATH];
 	char		bufin[MAX_STRING];
@@ -76,6 +76,7 @@ get_control_data(ClusterInfo *cluster, bool live_check)
 	uint32		segno = 0;
 	char	   *resetwal_bin;
 	int			rc;
+	bool		live_check = (cluster == &old_cluster && user_opts.live_check);
 
 	/*
 	 * Because we test the pg_resetwal output as strings, it has to be in
@@ -118,7 +119,7 @@ get_control_data(ClusterInfo *cluster, bool live_check)
 	/*
 	 * Check for clean shutdown
 	 */
-	if (!live_check || cluster == &new_cluster)
+	if (!live_check)
 	{
 		/* only pg_controldata outputs the cluster state */
 		snprintf(cmd, sizeof(cmd), "\"%s/pg_controldata\" \"%s\"",
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 95c22a7200..8f1777de59 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -27,7 +27,7 @@ static void free_rel_infos(RelInfoArr *rel_arr);
 static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
-static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check);
+static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo);
 static void get_db_subscription_count(DbInfo *dbinfo);
 
 
@@ -273,11 +273,9 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
  *
  * higher level routine to generate dbinfos for the database running
  * on the given "port". Assumes that server is already running.
- *
- * live_check would be used only when the target is the old cluster.
  */
 void
-get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check)
+get_db_rel_and_slot_infos(ClusterInfo *cluster)
 {
 	int			dbnum;
 
@@ -299,7 +297,7 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check)
 		 */
 		if (cluster == &old_cluster)
 		{
-			get_old_cluster_logical_slot_infos(pDbInfo, live_check);
+			get_old_cluster_logical_slot_infos(pDbInfo);
 			get_db_subscription_count(pDbInfo);
 		}
 	}
@@ -645,7 +643,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
  * are included.
  */
 static void
-get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
+get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -681,7 +679,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 							"WHERE slot_type = 'logical' AND "
 							"database = current_database() AND "
 							"temporary IS FALSE;",
-							live_check ? "FALSE" :
+							user_opts.live_check ? "FALSE" :
 							"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
 							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
 							"END)");
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index 548ea4e623..6f41d63eed 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -470,10 +470,10 @@ adjust_data_dir(ClusterInfo *cluster)
  * directory.
  */
 void
-get_sock_dir(ClusterInfo *cluster, bool live_check)
+get_sock_dir(ClusterInfo *cluster)
 {
 #if !defined(WIN32)
-	if (!live_check)
+	if (!user_opts.live_check || cluster == &new_cluster)
 		cluster->sockdir = user_opts.socketdir;
 	else
 	{
diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c
index af370768b6..3f4ad7d5cc 100644
--- a/src/bin/pg_upgrade/pg_upgrade.c
+++ b/src/bin/pg_upgrade/pg_upgrade.c
@@ -65,7 +65,7 @@ static void create_new_objects(void);
 static void copy_xact_xlog_xid(void);
 static void set_frozenxids(bool minmxid_only);
 static void make_outputdirs(char *pgdata);
-static void setup(char *argv0, bool *live_check);
+static void setup(char *argv0);
 static void create_logical_replication_slots(void);
 
 ClusterInfo old_cluster,
@@ -88,7 +88,6 @@ int
 main(int argc, char **argv)
 {
 	char	   *deletion_script_file_name = NULL;
-	bool		live_check = false;
 
 	/*
 	 * pg_upgrade doesn't currently use common/logging.c, but initialize it
@@ -123,18 +122,18 @@ main(int argc, char **argv)
 	 */
 	make_outputdirs(new_cluster.pgdata);
 
-	setup(argv[0], &live_check);
+	setup(argv[0]);
 
-	output_check_banner(live_check);
+	output_check_banner();
 
 	check_cluster_versions();
 
-	get_sock_dir(&old_cluster, live_check);
-	get_sock_dir(&new_cluster, false);
+	get_sock_dir(&old_cluster);
+	get_sock_dir(&new_cluster);
 
-	check_cluster_compatibility(live_check);
+	check_cluster_compatibility();
 
-	check_and_dump_old_cluster(live_check);
+	check_and_dump_old_cluster();
 
 
 	/* -- NEW -- */
@@ -331,7 +330,7 @@ make_outputdirs(char *pgdata)
 
 
 static void
-setup(char *argv0, bool *live_check)
+setup(char *argv0)
 {
 	/*
 	 * make sure the user has a clean environment, otherwise, we may confuse
@@ -378,7 +377,7 @@ setup(char *argv0, bool *live_check)
 				pg_fatal("There seems to be a postmaster servicing the old cluster.\n"
 						 "Please shutdown that postmaster and try again.");
 			else
-				*live_check = true;
+				user_opts.live_check = true;
 		}
 	}
 
@@ -648,7 +647,7 @@ create_new_objects(void)
 		set_frozenxids(true);
 
 	/* update new_cluster info now that we have objects in the databases */
-	get_db_rel_and_slot_infos(&new_cluster, false);
+	get_db_rel_and_slot_infos(&new_cluster);
 }
 
 /*
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 1ebad3bd74..56d05d7eb9 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -322,6 +322,7 @@ typedef struct
 typedef struct
 {
 	bool		check;			/* check clusters only, don't change any data */
+	bool		live_check;		/* check clusters only, old server is running */
 	bool		do_sync;		/* flush changes to disk */
 	transferMode transfer_mode; /* copy files or link them? */
 	int			jobs;			/* number of processes/threads to use */
@@ -366,20 +367,20 @@ extern OSInfo os_info;
 
 /* check.c */
 
-void		output_check_banner(bool live_check);
-void		check_and_dump_old_cluster(bool live_check);
+void		output_check_banner(void);
+void		check_and_dump_old_cluster(void);
 void		check_new_cluster(void);
 void		report_clusters_compatible(void);
 void		issue_warnings_and_set_wal_level(void);
 void		output_completion_banner(char *deletion_script_file_name);
 void		check_cluster_versions(void);
-void		check_cluster_compatibility(bool live_check);
+void		check_cluster_compatibility(void);
 void		create_script_for_old_cluster_deletion(char **deletion_script_file_name);
 
 
 /* controldata.c */
 
-void		get_control_data(ClusterInfo *cluster, bool live_check);
+void		get_control_data(ClusterInfo *cluster);
 void		check_control_data(ControlData *oldctrl, ControlData *newctrl);
 void		disable_old_cluster(void);
 
@@ -428,7 +429,7 @@ void		check_loadable_libraries(void);
 FileNameMap *gen_db_file_maps(DbInfo *old_db,
 							  DbInfo *new_db, int *nmaps, const char *old_pgdata,
 							  const char *new_pgdata);
-void		get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check);
+void		get_db_rel_and_slot_infos(ClusterInfo *cluster);
 int			count_old_cluster_logical_slots(void);
 int			count_old_cluster_subscriptions(void);
 
@@ -436,7 +437,7 @@ int			count_old_cluster_subscriptions(void);
 
 void		parseCommandLine(int argc, char *argv[]);
 void		adjust_data_dir(ClusterInfo *cluster);
-void		get_sock_dir(ClusterInfo *cluster, bool live_check);
+void		get_sock_dir(ClusterInfo *cluster);
 
 /* relfilenumber.c */
 
-- 
2.39.3 (Apple Git-146)

#5Robert Haas
robertmhaas@gmail.com
In reply to: Nathan Bossart (#4)
Re: optimizing pg_upgrade's once-in-each-database steps

On Mon, Jul 1, 2024 at 3:47 PM Nathan Bossart <nathandbossart@gmail.com> wrote:

0001 introduces a new API for registering callbacks and running them in
parallel on all databases in the cluster. This new system manages a set of
"slots" that follow a simple state machine to asynchronously establish a
connection and run the queries. It uses system() to wait for these
asynchronous tasks to complete. Users of this API only need to provide two
callbacks: one to return the query that should be run on each database and
another to process the results of that query. If multiple queries are
required for each database, users can provide multiple sets of callbacks.

I do really like the idea of using asynchronous communication here. It
should be significantly cheaper than using multiple processes or
threads, and maybe less code, too. But I'm confused about why you
would need or want to use system() to wait for asynchronous tasks to
complete. Wouldn't it be something like select()?

--
Robert Haas
EDB: http://www.enterprisedb.com

#6Nathan Bossart
nathandbossart@gmail.com
In reply to: Robert Haas (#5)
Re: optimizing pg_upgrade's once-in-each-database steps

On Mon, Jul 01, 2024 at 03:58:16PM -0400, Robert Haas wrote:

On Mon, Jul 1, 2024 at 3:47 PM Nathan Bossart <nathandbossart@gmail.com> wrote:

0001 introduces a new API for registering callbacks and running them in
parallel on all databases in the cluster. This new system manages a set of
"slots" that follow a simple state machine to asynchronously establish a
connection and run the queries. It uses system() to wait for these
asynchronous tasks to complete. Users of this API only need to provide two
callbacks: one to return the query that should be run on each database and
another to process the results of that query. If multiple queries are
required for each database, users can provide multiple sets of callbacks.

I do really like the idea of using asynchronous communication here. It
should be significantly cheaper than using multiple processes or
threads, and maybe less code, too. But I'm confused about why you
would need or want to use system() to wait for asynchronous tasks to
complete. Wouldn't it be something like select()?

Whoops, I meant to type "select()" there. Sorry for the confusion.

--
nathan

#7Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#6)
7 attachment(s)
Re: optimizing pg_upgrade's once-in-each-database steps

As I mentioned elsewhere [0]/messages/by-id/Zov5kHbEyDMuHJI_@nathan, here's a first attempt at parallelizing the
data type checks. I was worried that I might have to refactor Daniel's
work in commit 347758b quite significantly, but I was able to avoid that by
using a set of generic callbacks and providing each task step an index to
the data_types_usage_checks array.

[0]: /messages/by-id/Zov5kHbEyDMuHJI_@nathan

--
nathan

Attachments:

v3-0001-introduce-framework-for-parallelizing-pg_upgrade-.patchtext/plain; charset=us-asciiDownload
From 0d3353c318ca4dcd582d67ca25c47b9e3cf24921 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 11:02:44 -0500
Subject: [PATCH v3 1/7] introduce framework for parallelizing pg_upgrade tasks

---
 src/bin/pg_upgrade/Makefile      |   1 +
 src/bin/pg_upgrade/async.c       | 323 +++++++++++++++++++++++++++++++
 src/bin/pg_upgrade/meson.build   |   1 +
 src/bin/pg_upgrade/pg_upgrade.h  |  16 ++
 src/tools/pgindent/typedefs.list |   4 +
 5 files changed, 345 insertions(+)
 create mode 100644 src/bin/pg_upgrade/async.c

diff --git a/src/bin/pg_upgrade/Makefile b/src/bin/pg_upgrade/Makefile
index bde91e2beb..3bc4f5d740 100644
--- a/src/bin/pg_upgrade/Makefile
+++ b/src/bin/pg_upgrade/Makefile
@@ -12,6 +12,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	$(WIN32RES) \
+	async.o \
 	check.o \
 	controldata.o \
 	dump.o \
diff --git a/src/bin/pg_upgrade/async.c b/src/bin/pg_upgrade/async.c
new file mode 100644
index 0000000000..7df1e7712d
--- /dev/null
+++ b/src/bin/pg_upgrade/async.c
@@ -0,0 +1,323 @@
+/*
+ * async.c
+ *
+ * parallelization via libpq's async APIs
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ * src/bin/pg_upgrade/async.c
+ */
+
+#include "postgres_fe.h"
+
+#include "common/connect.h"
+#include "fe_utils/string_utils.h"
+#include "pg_upgrade.h"
+
+static int	dbs_complete;
+static int	dbs_processing;
+
+typedef struct AsyncTaskCallbacks
+{
+	AsyncTaskGetQueryCB query_cb;
+	AsyncTaskProcessCB process_cb;
+	bool		free_result;
+	void	   *arg;
+} AsyncTaskCallbacks;
+
+typedef struct AsyncTask
+{
+	AsyncTaskCallbacks *cbs;
+	int			num_cb_sets;
+} AsyncTask;
+
+typedef enum
+{
+	FREE,
+	CONNECTING,
+	SETTING_SEARCH_PATH,
+	RUNNING_QUERY,
+} AsyncSlotState;
+
+typedef struct
+{
+	AsyncSlotState state;
+	int			db;
+	int			query;
+	PGconn	   *conn;
+} AsyncSlot;
+
+AsyncTask *
+async_task_create(void)
+{
+	return pg_malloc0(sizeof(AsyncTask));
+}
+
+void
+async_task_free(AsyncTask *task)
+{
+	if (task->cbs)
+		pg_free(task->cbs);
+
+	pg_free(task);
+}
+
+void
+async_task_add_step(AsyncTask *task,
+					AsyncTaskGetQueryCB query_cb,
+					AsyncTaskProcessCB process_cb, bool free_result,
+					void *arg)
+{
+	AsyncTaskCallbacks *new_cbs;
+
+	task->cbs = pg_realloc(task->cbs,
+						   ++task->num_cb_sets * sizeof(AsyncTaskCallbacks));
+
+	new_cbs = &task->cbs[task->num_cb_sets - 1];
+	new_cbs->query_cb = query_cb;
+	new_cbs->process_cb = process_cb;
+	new_cbs->free_result = free_result;
+	new_cbs->arg = arg;
+}
+
+static void
+conn_failure(PGconn *conn)
+{
+	pg_log(PG_REPORT, "%s", PQerrorMessage(conn));
+	printf(_("Failure, exiting\n"));
+	exit(1);
+}
+
+static void
+start_conn(const ClusterInfo *cluster, AsyncSlot *slot)
+{
+	PQExpBufferData conn_opts;
+
+	/* Build connection string with proper quoting */
+	initPQExpBuffer(&conn_opts);
+	appendPQExpBufferStr(&conn_opts, "dbname=");
+	appendConnStrVal(&conn_opts, cluster->dbarr.dbs[slot->db].db_name);
+	appendPQExpBufferStr(&conn_opts, " user=");
+	appendConnStrVal(&conn_opts, os_info.user);
+	appendPQExpBuffer(&conn_opts, " port=%d", cluster->port);
+	if (cluster->sockdir)
+	{
+		appendPQExpBufferStr(&conn_opts, " host=");
+		appendConnStrVal(&conn_opts, cluster->sockdir);
+	}
+
+	slot->conn = PQconnectStart(conn_opts.data);
+	termPQExpBuffer(&conn_opts);
+
+	if (!slot->conn)
+		conn_failure(slot->conn);
+}
+
+static void
+dispatch_query(const ClusterInfo *cluster, AsyncSlot *slot,
+			   const AsyncTask *task)
+{
+	AsyncTaskCallbacks *cbs = &task->cbs[slot->query];
+	AsyncTaskGetQueryCB get_query = cbs->query_cb;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db];
+	char	   *query = (*get_query) (dbinfo, cbs->arg);
+
+	if (!PQsendQuery(slot->conn, query))
+		conn_failure(slot->conn);
+
+	pg_free(query);
+}
+
+static PGresult *
+get_last_result(PGconn *conn)
+{
+	PGresult   *tmp;
+	PGresult   *res = NULL;
+
+	while ((tmp = PQgetResult(conn)) != NULL)
+	{
+		PQclear(res);
+		res = tmp;
+		if (PQstatus(conn) == CONNECTION_BAD)
+			conn_failure(conn);
+	}
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK &&
+		PQresultStatus(res) != PGRES_TUPLES_OK)
+		conn_failure(conn);
+
+	return res;
+}
+
+static void
+process_query_result(const ClusterInfo *cluster, AsyncSlot *slot,
+					 const AsyncTask *task)
+{
+	AsyncTaskCallbacks *cbs = &task->cbs[slot->query];
+	AsyncTaskProcessCB process_cb = cbs->process_cb;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db];
+	PGresult   *res = get_last_result(slot->conn);
+
+	(*process_cb) (dbinfo, res, cbs->arg);
+
+	if (cbs->free_result)
+		PQclear(res);
+}
+
+static void
+process_slot(const ClusterInfo *cluster, AsyncSlot *slot, const AsyncTask *task)
+{
+	switch (slot->state)
+	{
+		case FREE:
+			if (dbs_processing >= cluster->dbarr.ndbs)
+				return;
+			slot->db = dbs_processing++;
+			slot->state = CONNECTING;
+			start_conn(cluster, slot);
+			return;
+
+		case CONNECTING:
+			if (PQconnectPoll(slot->conn) == PGRES_POLLING_FAILED)
+				conn_failure(slot->conn);
+			if (PQconnectPoll(slot->conn) != PGRES_POLLING_OK)
+				return;
+			slot->state = SETTING_SEARCH_PATH;
+			if (!PQsendQuery(slot->conn, ALWAYS_SECURE_SEARCH_PATH_SQL))
+				conn_failure(slot->conn);
+			return;
+
+		case SETTING_SEARCH_PATH:
+			if (!PQconsumeInput(slot->conn))
+				conn_failure(slot->conn);
+			if (PQisBusy(slot->conn))
+				return;
+			PQclear(get_last_result(slot->conn));
+			slot->state = RUNNING_QUERY;
+			dispatch_query(cluster, slot, task);
+			return;
+
+		case RUNNING_QUERY:
+			if (!PQconsumeInput(slot->conn))
+				conn_failure(slot->conn);
+			if (PQisBusy(slot->conn))
+				return;
+			process_query_result(cluster, slot, task);
+			if (++slot->query >= task->num_cb_sets)
+			{
+				dbs_complete++;
+				PQfinish(slot->conn);
+				memset(slot, 0, sizeof(AsyncSlot));
+				return;
+			}
+			dispatch_query(cluster, slot, task);
+			return;
+	}
+}
+
+/*
+ * Wait on the slots to either finish connecting or to receive query results if
+ * possible.  This avoids a tight loop in async_task_run().
+ */
+static void
+wait_on_slots(AsyncSlot *slots, int numslots)
+{
+	fd_set		input_mask;
+	fd_set		output_mask;
+	fd_set		except_mask;
+	int			maxFd = 0;
+
+	FD_ZERO(&input_mask);
+	FD_ZERO(&output_mask);
+	FD_ZERO(&except_mask);
+
+	for (int i = 0; i < numslots; i++)
+	{
+		int			sock;
+		bool		read = false;
+
+		switch (slots[i].state)
+		{
+			case FREE:
+
+				/*
+				 * If we see a free slot, return right away so that it can be
+				 * reused immediately for the next database.  This might cause
+				 * us to spin more than necessary as we finish processing the
+				 * last few databases, but that shouldn't cause too much harm.
+				 */
+				return;
+
+			case CONNECTING:
+
+				/*
+				 * If we are waiting for the connection to establish, choose
+				 * whether to wait for reading or for writing on the socket as
+				 * appropriate.  If neither apply, just return immediately so
+				 * that we can handle the slot.
+				 */
+				{
+					PostgresPollingStatusType status;
+
+					status = PQconnectPoll(slots[i].conn);
+					if (status == PGRES_POLLING_READING)
+						read = true;
+					else if (status != PGRES_POLLING_WRITING)
+						return;
+				}
+				break;
+
+			case SETTING_SEARCH_PATH:
+			case RUNNING_QUERY:
+
+				/*
+				 * If we've sent a query, we must wait for the socket to be
+				 * read-ready.  Note that process_slot() handles calling
+				 * PQconsumeInput() as required.
+				 */
+				read = true;
+				break;
+		}
+
+		/*
+		 * If there's some problem retrieving the socket, just pretend this
+		 * slot doesn't exist.  We don't expect this to happen regularly in
+		 * practice, so it seems unlikely to cause too much harm.
+		 */
+		sock = PQsocket(slots[i].conn);
+		if (sock < 0)
+			continue;
+
+		/*
+		 * Add the socket to the set.
+		 */
+		FD_SET(sock, read ? &input_mask : &output_mask);
+		FD_SET(sock, &except_mask);
+		maxFd = Max(maxFd, sock);
+	}
+
+	/*
+	 * If we found socket(s) to wait on, wait.
+	 */
+	if (maxFd != 0)
+		(void) select(maxFd + 1, &input_mask, &output_mask, &except_mask, NULL);
+}
+
+void
+async_task_run(const AsyncTask *task, const ClusterInfo *cluster)
+{
+	int			jobs = Max(1, user_opts.jobs);
+	AsyncSlot  *slots = pg_malloc0(sizeof(AsyncSlot) * jobs);
+
+	dbs_complete = 0;
+	dbs_processing = 0;
+
+	while (dbs_complete < cluster->dbarr.ndbs)
+	{
+		for (int i = 0; i < jobs; i++)
+			process_slot(cluster, &slots[i], task);
+
+		wait_on_slots(slots, jobs);
+	}
+
+	pg_free(slots);
+}
diff --git a/src/bin/pg_upgrade/meson.build b/src/bin/pg_upgrade/meson.build
index 9825fa3305..9eb48e176c 100644
--- a/src/bin/pg_upgrade/meson.build
+++ b/src/bin/pg_upgrade/meson.build
@@ -1,6 +1,7 @@
 # Copyright (c) 2022-2024, PostgreSQL Global Development Group
 
 pg_upgrade_sources = files(
+  'async.c',
   'check.c',
   'controldata.c',
   'dump.c',
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 8afe240bdf..1ebad3bd74 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -494,3 +494,19 @@ void		parallel_transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr
 										  char *old_pgdata, char *new_pgdata,
 										  char *old_tablespace);
 bool		reap_child(bool wait_for_child);
+
+/* async.c */
+
+typedef char *(*AsyncTaskGetQueryCB) (DbInfo *dbinfo, void *arg);
+typedef void (*AsyncTaskProcessCB) (DbInfo *dbinfo, PGresult *res, void *arg);
+
+/* struct definition is private to async.c */
+typedef struct AsyncTask AsyncTask;
+
+AsyncTask  *async_task_create(void);
+void		async_task_add_step(AsyncTask *task,
+								AsyncTaskGetQueryCB query_cb,
+								AsyncTaskProcessCB process_cb, bool free_result,
+								void *arg);
+void		async_task_run(const AsyncTask *task, const ClusterInfo *cluster);
+void		async_task_free(AsyncTask *task);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9320e4d808..ef0db74d3b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -153,6 +153,10 @@ ArrayMetaState
 ArraySubWorkspace
 ArrayToken
 ArrayType
+AsyncSlot
+AsyncSlotState
+AsyncTask
+AsyncTaskCallbacks
 AsyncQueueControl
 AsyncQueueEntry
 AsyncRequest
-- 
2.39.3 (Apple Git-146)

v3-0002-use-new-pg_upgrade-async-API-for-subscription-sta.patchtext/plain; charset=us-asciiDownload
From 2b651e53dcde039aec716de05ce70a0a9a69fef6 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 17:21:19 -0500
Subject: [PATCH v3 2/7] use new pg_upgrade async API for subscription state
 checks

---
 src/bin/pg_upgrade/check.c | 200 ++++++++++++++++++++-----------------
 1 file changed, 106 insertions(+), 94 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 27924159d6..f653fa25a5 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1906,6 +1906,75 @@ check_old_cluster_for_valid_slots(bool live_check)
 	check_ok();
 }
 
+/* private state for subscription state checks */
+struct substate_info
+{
+	FILE	   *script;
+	char		output_path[MAXPGPATH];
+};
+
+/*
+ * We don't allow upgrade if there is a risk of dangling slot or origin
+ * corresponding to initial sync after upgrade.
+ *
+ * A slot/origin not created yet refers to the 'i' (initialize) state, while
+ * 'r' (ready) state refers to a slot/origin created previously but already
+ * dropped. These states are supported for pg_upgrade. The other states listed
+ * below are not supported:
+ *
+ * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state would
+ * retain a replication slot, which could not be dropped by the sync worker
+ * spawned after the upgrade because the subscription ID used for the slot name
+ * won't match anymore.
+ *
+ * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state would
+ * retain the replication origin when there is a failure in tablesync worker
+ * immediately after dropping the replication slot in the publisher.
+ *
+ * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on a
+ * relation upgraded while in this state would expect an origin ID with the OID
+ * of the subscription used before the upgrade, causing it to fail.
+ *
+ * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and SUBREL_STATE_UNKNOWN:
+ * These states are not stored in the catalog, so we need not allow these
+ * states.
+ */
+static char *
+sub_query(DbInfo *dbinfo, void *arg)
+{
+	return pg_strdup("SELECT r.srsubstate, s.subname, n.nspname, c.relname "
+					 "FROM pg_catalog.pg_subscription_rel r "
+					 "LEFT JOIN pg_catalog.pg_subscription s"
+					 "   ON r.srsubid = s.oid "
+					 "LEFT JOIN pg_catalog.pg_class c"
+					 "   ON r.srrelid = c.oid "
+					 "LEFT JOIN pg_catalog.pg_namespace n"
+					 "   ON c.relnamespace = n.oid "
+					 "WHERE r.srsubstate NOT IN ('i', 'r') "
+					 "ORDER BY s.subname");
+}
+
+static void
+sub_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct substate_info *state = (struct substate_info *) arg;
+	int			ntup = PQntuples(res);
+
+	for (int i = 0; i < ntup; i++)
+	{
+		if (state->script == NULL &&
+			(state->script = fopen_priv(state->output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state->output_path);
+
+		fprintf(state->script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
+				PQgetvalue(res, i, 0),
+				dbinfo->db_name,
+				PQgetvalue(res, i, 1),
+				PQgetvalue(res, i, 2),
+				PQgetvalue(res, i, 3));
+	}
+}
+
 /*
  * check_old_cluster_subscription_state()
  *
@@ -1916,115 +1985,58 @@ check_old_cluster_for_valid_slots(bool live_check)
 static void
 check_old_cluster_subscription_state(void)
 {
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
+	struct substate_info state;
+	PGresult   *res;
+	PGconn	   *conn;
 	int			ntup;
 
 	prep_status("Checking for subscription state");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	state.script = NULL;
+	snprintf(state.output_path, sizeof(state.output_path), "%s/%s",
 			 log_opts.basedir,
 			 "subs_invalid.txt");
-	for (int dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
 
-		/* We need to check for pg_replication_origin only once. */
-		if (dbnum == 0)
-		{
-			/*
-			 * Check that all the subscriptions have their respective
-			 * replication origin.
-			 */
-			res = executeQueryOrDie(conn,
-									"SELECT d.datname, s.subname "
-									"FROM pg_catalog.pg_subscription s "
-									"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
-									"	ON o.roname = 'pg_' || s.oid "
-									"INNER JOIN pg_catalog.pg_database d "
-									"	ON d.oid = s.subdbid "
-									"WHERE o.roname IS NULL;");
-
-			ntup = PQntuples(res);
-			for (int i = 0; i < ntup; i++)
-			{
-				if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-					pg_fatal("could not open file \"%s\": %m", output_path);
-				fprintf(script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
-						PQgetvalue(res, i, 0),
-						PQgetvalue(res, i, 1));
-			}
-			PQclear(res);
-		}
-
-		/*
-		 * We don't allow upgrade if there is a risk of dangling slot or
-		 * origin corresponding to initial sync after upgrade.
-		 *
-		 * A slot/origin not created yet refers to the 'i' (initialize) state,
-		 * while 'r' (ready) state refers to a slot/origin created previously
-		 * but already dropped. These states are supported for pg_upgrade. The
-		 * other states listed below are not supported:
-		 *
-		 * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state
-		 * would retain a replication slot, which could not be dropped by the
-		 * sync worker spawned after the upgrade because the subscription ID
-		 * used for the slot name won't match anymore.
-		 *
-		 * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state
-		 * would retain the replication origin when there is a failure in
-		 * tablesync worker immediately after dropping the replication slot in
-		 * the publisher.
-		 *
-		 * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on
-		 * a relation upgraded while in this state would expect an origin ID
-		 * with the OID of the subscription used before the upgrade, causing
-		 * it to fail.
-		 *
-		 * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and
-		 * SUBREL_STATE_UNKNOWN: These states are not stored in the catalog,
-		 * so we need not allow these states.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT r.srsubstate, s.subname, n.nspname, c.relname "
-								"FROM pg_catalog.pg_subscription_rel r "
-								"LEFT JOIN pg_catalog.pg_subscription s"
-								"	ON r.srsubid = s.oid "
-								"LEFT JOIN pg_catalog.pg_class c"
-								"	ON r.srrelid = c.oid "
-								"LEFT JOIN pg_catalog.pg_namespace n"
-								"	ON c.relnamespace = n.oid "
-								"WHERE r.srsubstate NOT IN ('i', 'r') "
-								"ORDER BY s.subname");
-
-		ntup = PQntuples(res);
-		for (int i = 0; i < ntup; i++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
+	/*
+	 * Check that all the subscriptions have their respective replication
+	 * origin.  This check only needs to run once.
+	 */
+	conn = connectToServer(&old_cluster, old_cluster.dbarr.dbs[0].db_name);
+	res = executeQueryOrDie(conn,
+							"SELECT d.datname, s.subname "
+							"FROM pg_catalog.pg_subscription s "
+							"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
+							"	ON o.roname = 'pg_' || s.oid "
+							"INNER JOIN pg_catalog.pg_database d "
+							"	ON d.oid = s.subdbid "
+							"WHERE o.roname IS NULL;");
+	ntup = PQntuples(res);
+	for (int i = 0; i < ntup; i++)
+	{
+		if (state.script == NULL &&
+			(state.script = fopen_priv(state.output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state.output_path);
+		fprintf(state.script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
+				PQgetvalue(res, i, 0),
+				PQgetvalue(res, i, 1));
+	}
+	PQclear(res);
+	PQfinish(conn);
 
-			fprintf(script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
-					PQgetvalue(res, i, 0),
-					active_db->db_name,
-					PQgetvalue(res, i, 1),
-					PQgetvalue(res, i, 2),
-					PQgetvalue(res, i, 3));
-		}
+	async_task_add_step(task, sub_query, sub_process, true, &state);
 
-		PQclear(res);
-		PQfinish(conn);
-	}
+	async_task_run(task, &old_cluster);
+	async_task_free(task);
 
-	if (script)
+	if (state.script)
 	{
-		fclose(script);
+		fclose(state.script);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains subscriptions without origin or having relations not in i (initialize) or r (ready) state.\n"
 				 "You can allow the initial sync to finish for all relations and then restart the upgrade.\n"
 				 "A list of the problematic subscriptions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", state.output_path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v3-0003-move-live_check-variable-to-user_opts.patchtext/plain; charset=us-asciiDownload
From 753f3399de8eabd38b79e61f6e1880547d1121c0 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 21 May 2024 16:35:19 -0500
Subject: [PATCH v3 3/7] move live_check variable to user_opts

---
 src/bin/pg_upgrade/check.c       | 32 ++++++++++++++++----------------
 src/bin/pg_upgrade/controldata.c |  5 +++--
 src/bin/pg_upgrade/info.c        | 12 +++++-------
 src/bin/pg_upgrade/option.c      |  4 ++--
 src/bin/pg_upgrade/pg_upgrade.c  | 21 ++++++++++-----------
 src/bin/pg_upgrade/pg_upgrade.h  | 13 +++++++------
 6 files changed, 43 insertions(+), 44 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index f653fa25a5..251f3d9017 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -29,7 +29,7 @@ static void check_for_new_tablespace_dir(void);
 static void check_for_user_defined_encoding_conversions(ClusterInfo *cluster);
 static void check_new_cluster_logical_replication_slots(void);
 static void check_new_cluster_subscription_configuration(void);
-static void check_old_cluster_for_valid_slots(bool live_check);
+static void check_old_cluster_for_valid_slots(void);
 static void check_old_cluster_subscription_state(void);
 
 /*
@@ -555,9 +555,9 @@ fix_path_separator(char *path)
 }
 
 void
-output_check_banner(bool live_check)
+output_check_banner(void)
 {
-	if (user_opts.check && live_check)
+	if (user_opts.live_check)
 	{
 		pg_log(PG_REPORT,
 			   "Performing Consistency Checks on Old Live Server\n"
@@ -573,18 +573,18 @@ output_check_banner(bool live_check)
 
 
 void
-check_and_dump_old_cluster(bool live_check)
+check_and_dump_old_cluster(void)
 {
 	/* -- OLD -- */
 
-	if (!live_check)
+	if (!user_opts.live_check)
 		start_postmaster(&old_cluster, true);
 
 	/*
 	 * Extract a list of databases, tables, and logical replication slots from
 	 * the old cluster.
 	 */
-	get_db_rel_and_slot_infos(&old_cluster, live_check);
+	get_db_rel_and_slot_infos(&old_cluster);
 
 	init_tablespaces();
 
@@ -605,7 +605,7 @@ check_and_dump_old_cluster(bool live_check)
 		 * Logical replication slots can be migrated since PG17. See comments
 		 * atop get_old_cluster_logical_slot_infos().
 		 */
-		check_old_cluster_for_valid_slots(live_check);
+		check_old_cluster_for_valid_slots();
 
 		/*
 		 * Subscriptions and their dependencies can be migrated since PG17.
@@ -652,7 +652,7 @@ check_and_dump_old_cluster(bool live_check)
 	 */
 	if (GET_MAJOR_VERSION(old_cluster.major_version) <= 906)
 	{
-		if (user_opts.check)
+		if (user_opts.live_check)
 			old_9_6_invalidate_hash_indexes(&old_cluster, true);
 	}
 
@@ -667,7 +667,7 @@ check_and_dump_old_cluster(bool live_check)
 	if (!user_opts.check)
 		generate_old_dump();
 
-	if (!live_check)
+	if (!user_opts.live_check)
 		stop_postmaster(false);
 }
 
@@ -675,7 +675,7 @@ check_and_dump_old_cluster(bool live_check)
 void
 check_new_cluster(void)
 {
-	get_db_rel_and_slot_infos(&new_cluster, false);
+	get_db_rel_and_slot_infos(&new_cluster);
 
 	check_new_cluster_is_empty();
 
@@ -826,14 +826,14 @@ check_cluster_versions(void)
 
 
 void
-check_cluster_compatibility(bool live_check)
+check_cluster_compatibility(void)
 {
 	/* get/check pg_control data of servers */
-	get_control_data(&old_cluster, live_check);
-	get_control_data(&new_cluster, false);
+	get_control_data(&old_cluster);
+	get_control_data(&new_cluster);
 	check_control_data(&old_cluster.controldata, &new_cluster.controldata);
 
-	if (live_check && old_cluster.port == new_cluster.port)
+	if (user_opts.live_check && old_cluster.port == new_cluster.port)
 		pg_fatal("When checking a live server, "
 				 "the old and new port numbers must be different.");
 }
@@ -1839,7 +1839,7 @@ check_new_cluster_subscription_configuration(void)
  * before shutdown.
  */
 static void
-check_old_cluster_for_valid_slots(bool live_check)
+check_old_cluster_for_valid_slots(void)
 {
 	char		output_path[MAXPGPATH];
 	FILE	   *script = NULL;
@@ -1878,7 +1878,7 @@ check_old_cluster_for_valid_slots(bool live_check)
 			 * Note: This can be satisfied only when the old cluster has been
 			 * shut down, so we skip this for live checks.
 			 */
-			if (!live_check && !slot->caught_up)
+			if (!user_opts.live_check && !slot->caught_up)
 			{
 				if (script == NULL &&
 					(script = fopen_priv(output_path, "w")) == NULL)
diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c
index 1f0ccea3ed..cf665b9dee 100644
--- a/src/bin/pg_upgrade/controldata.c
+++ b/src/bin/pg_upgrade/controldata.c
@@ -33,7 +33,7 @@
  * return valid xid data for a running server.
  */
 void
-get_control_data(ClusterInfo *cluster, bool live_check)
+get_control_data(ClusterInfo *cluster)
 {
 	char		cmd[MAXPGPATH];
 	char		bufin[MAX_STRING];
@@ -76,6 +76,7 @@ get_control_data(ClusterInfo *cluster, bool live_check)
 	uint32		segno = 0;
 	char	   *resetwal_bin;
 	int			rc;
+	bool		live_check = (cluster == &old_cluster && user_opts.live_check);
 
 	/*
 	 * Because we test the pg_resetwal output as strings, it has to be in
@@ -118,7 +119,7 @@ get_control_data(ClusterInfo *cluster, bool live_check)
 	/*
 	 * Check for clean shutdown
 	 */
-	if (!live_check || cluster == &new_cluster)
+	if (!live_check)
 	{
 		/* only pg_controldata outputs the cluster state */
 		snprintf(cmd, sizeof(cmd), "\"%s/pg_controldata\" \"%s\"",
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 95c22a7200..8f1777de59 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -27,7 +27,7 @@ static void free_rel_infos(RelInfoArr *rel_arr);
 static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
-static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check);
+static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo);
 static void get_db_subscription_count(DbInfo *dbinfo);
 
 
@@ -273,11 +273,9 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
  *
  * higher level routine to generate dbinfos for the database running
  * on the given "port". Assumes that server is already running.
- *
- * live_check would be used only when the target is the old cluster.
  */
 void
-get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check)
+get_db_rel_and_slot_infos(ClusterInfo *cluster)
 {
 	int			dbnum;
 
@@ -299,7 +297,7 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check)
 		 */
 		if (cluster == &old_cluster)
 		{
-			get_old_cluster_logical_slot_infos(pDbInfo, live_check);
+			get_old_cluster_logical_slot_infos(pDbInfo);
 			get_db_subscription_count(pDbInfo);
 		}
 	}
@@ -645,7 +643,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
  * are included.
  */
 static void
-get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
+get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -681,7 +679,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 							"WHERE slot_type = 'logical' AND "
 							"database = current_database() AND "
 							"temporary IS FALSE;",
-							live_check ? "FALSE" :
+							user_opts.live_check ? "FALSE" :
 							"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
 							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
 							"END)");
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index 548ea4e623..6f41d63eed 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -470,10 +470,10 @@ adjust_data_dir(ClusterInfo *cluster)
  * directory.
  */
 void
-get_sock_dir(ClusterInfo *cluster, bool live_check)
+get_sock_dir(ClusterInfo *cluster)
 {
 #if !defined(WIN32)
-	if (!live_check)
+	if (!user_opts.live_check || cluster == &new_cluster)
 		cluster->sockdir = user_opts.socketdir;
 	else
 	{
diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c
index af370768b6..3f4ad7d5cc 100644
--- a/src/bin/pg_upgrade/pg_upgrade.c
+++ b/src/bin/pg_upgrade/pg_upgrade.c
@@ -65,7 +65,7 @@ static void create_new_objects(void);
 static void copy_xact_xlog_xid(void);
 static void set_frozenxids(bool minmxid_only);
 static void make_outputdirs(char *pgdata);
-static void setup(char *argv0, bool *live_check);
+static void setup(char *argv0);
 static void create_logical_replication_slots(void);
 
 ClusterInfo old_cluster,
@@ -88,7 +88,6 @@ int
 main(int argc, char **argv)
 {
 	char	   *deletion_script_file_name = NULL;
-	bool		live_check = false;
 
 	/*
 	 * pg_upgrade doesn't currently use common/logging.c, but initialize it
@@ -123,18 +122,18 @@ main(int argc, char **argv)
 	 */
 	make_outputdirs(new_cluster.pgdata);
 
-	setup(argv[0], &live_check);
+	setup(argv[0]);
 
-	output_check_banner(live_check);
+	output_check_banner();
 
 	check_cluster_versions();
 
-	get_sock_dir(&old_cluster, live_check);
-	get_sock_dir(&new_cluster, false);
+	get_sock_dir(&old_cluster);
+	get_sock_dir(&new_cluster);
 
-	check_cluster_compatibility(live_check);
+	check_cluster_compatibility();
 
-	check_and_dump_old_cluster(live_check);
+	check_and_dump_old_cluster();
 
 
 	/* -- NEW -- */
@@ -331,7 +330,7 @@ make_outputdirs(char *pgdata)
 
 
 static void
-setup(char *argv0, bool *live_check)
+setup(char *argv0)
 {
 	/*
 	 * make sure the user has a clean environment, otherwise, we may confuse
@@ -378,7 +377,7 @@ setup(char *argv0, bool *live_check)
 				pg_fatal("There seems to be a postmaster servicing the old cluster.\n"
 						 "Please shutdown that postmaster and try again.");
 			else
-				*live_check = true;
+				user_opts.live_check = true;
 		}
 	}
 
@@ -648,7 +647,7 @@ create_new_objects(void)
 		set_frozenxids(true);
 
 	/* update new_cluster info now that we have objects in the databases */
-	get_db_rel_and_slot_infos(&new_cluster, false);
+	get_db_rel_and_slot_infos(&new_cluster);
 }
 
 /*
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 1ebad3bd74..56d05d7eb9 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -322,6 +322,7 @@ typedef struct
 typedef struct
 {
 	bool		check;			/* check clusters only, don't change any data */
+	bool		live_check;		/* check clusters only, old server is running */
 	bool		do_sync;		/* flush changes to disk */
 	transferMode transfer_mode; /* copy files or link them? */
 	int			jobs;			/* number of processes/threads to use */
@@ -366,20 +367,20 @@ extern OSInfo os_info;
 
 /* check.c */
 
-void		output_check_banner(bool live_check);
-void		check_and_dump_old_cluster(bool live_check);
+void		output_check_banner(void);
+void		check_and_dump_old_cluster(void);
 void		check_new_cluster(void);
 void		report_clusters_compatible(void);
 void		issue_warnings_and_set_wal_level(void);
 void		output_completion_banner(char *deletion_script_file_name);
 void		check_cluster_versions(void);
-void		check_cluster_compatibility(bool live_check);
+void		check_cluster_compatibility(void);
 void		create_script_for_old_cluster_deletion(char **deletion_script_file_name);
 
 
 /* controldata.c */
 
-void		get_control_data(ClusterInfo *cluster, bool live_check);
+void		get_control_data(ClusterInfo *cluster);
 void		check_control_data(ControlData *oldctrl, ControlData *newctrl);
 void		disable_old_cluster(void);
 
@@ -428,7 +429,7 @@ void		check_loadable_libraries(void);
 FileNameMap *gen_db_file_maps(DbInfo *old_db,
 							  DbInfo *new_db, int *nmaps, const char *old_pgdata,
 							  const char *new_pgdata);
-void		get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check);
+void		get_db_rel_and_slot_infos(ClusterInfo *cluster);
 int			count_old_cluster_logical_slots(void);
 int			count_old_cluster_subscriptions(void);
 
@@ -436,7 +437,7 @@ int			count_old_cluster_subscriptions(void);
 
 void		parseCommandLine(int argc, char *argv[]);
 void		adjust_data_dir(ClusterInfo *cluster);
-void		get_sock_dir(ClusterInfo *cluster, bool live_check);
+void		get_sock_dir(ClusterInfo *cluster);
 
 /* relfilenumber.c */
 
-- 
2.39.3 (Apple Git-146)

v3-0004-use-new-pg_upgrade-async-API-for-retrieving-relin.patchtext/plain; charset=us-asciiDownload
From 7b72cf76faba3532499e0f4d6ba1f5035f3fe1b6 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:09:33 -0500
Subject: [PATCH v3 4/7] use new pg_upgrade async API for retrieving relinfos

---
 src/bin/pg_upgrade/info.c | 187 +++++++++++++++++---------------------
 1 file changed, 81 insertions(+), 106 deletions(-)

diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 8f1777de59..d07255bd0a 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -22,13 +22,16 @@ static void report_unmatched_relation(const RelInfo *rel, const DbInfo *db,
 static void free_db_and_rel_infos(DbInfoArr *db_arr);
 static void get_template0_info(ClusterInfo *cluster);
 static void get_db_infos(ClusterInfo *cluster);
-static void get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo);
+static char *get_rel_infos_query(DbInfo *dbinfo, void *arg);
+static void get_rel_infos_result(DbInfo *dbinfo, PGresult *res, void *arg);
 static void free_rel_infos(RelInfoArr *rel_arr);
 static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
-static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo);
-static void get_db_subscription_count(DbInfo *dbinfo);
+static char *get_old_cluster_logical_slot_infos_query(DbInfo *dbinfo, void *arg);
+static void get_old_cluster_logical_slot_infos_result(DbInfo *dbinfo, PGresult *res, void *arg);
+static char *get_db_subscription_count_query(DbInfo *dbinfo, void *arg);
+static void get_db_subscription_count_result(DbInfo *dbinfo, PGresult *res, void *arg);
 
 
 /*
@@ -277,7 +280,7 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
 void
 get_db_rel_and_slot_infos(ClusterInfo *cluster)
 {
-	int			dbnum;
+	AsyncTask  *task = async_task_create();
 
 	if (cluster->dbarr.dbs != NULL)
 		free_db_and_rel_infos(&cluster->dbarr);
@@ -285,23 +288,26 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster)
 	get_template0_info(cluster);
 	get_db_infos(cluster);
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	async_task_add_step(task,
+						get_rel_infos_query,
+						get_rel_infos_result,
+						true, NULL);
+	if (cluster == &old_cluster &&
+		GET_MAJOR_VERSION(cluster->major_version) > 1600)
 	{
-		DbInfo	   *pDbInfo = &cluster->dbarr.dbs[dbnum];
-
-		get_rel_infos(cluster, pDbInfo);
-
-		/*
-		 * Retrieve the logical replication slots infos and the subscriptions
-		 * count for the old cluster.
-		 */
-		if (cluster == &old_cluster)
-		{
-			get_old_cluster_logical_slot_infos(pDbInfo);
-			get_db_subscription_count(pDbInfo);
-		}
+		async_task_add_step(task,
+							get_old_cluster_logical_slot_infos_query,
+							get_old_cluster_logical_slot_infos_result,
+							true, cluster);
+		async_task_add_step(task,
+							get_db_subscription_count_query,
+							get_db_subscription_count_result,
+							true, cluster);
 	}
 
+	async_task_run(task, cluster);
+	async_task_free(task);
+
 	if (cluster == &old_cluster)
 		pg_log(PG_VERBOSE, "\nsource databases:");
 	else
@@ -447,30 +453,10 @@ get_db_infos(ClusterInfo *cluster)
  * Note: the resulting RelInfo array is assumed to be sorted by OID.
  * This allows later processing to match up old and new databases efficiently.
  */
-static void
-get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
+static char *
+get_rel_infos_query(DbInfo *dbinfo, void *arg)
 {
-	PGconn	   *conn = connectToServer(cluster,
-									   dbinfo->db_name);
-	PGresult   *res;
-	RelInfo    *relinfos;
-	int			ntups;
-	int			relnum;
-	int			num_rels = 0;
-	char	   *nspname = NULL;
-	char	   *relname = NULL;
-	char	   *tablespace = NULL;
-	int			i_spclocation,
-				i_nspname,
-				i_relname,
-				i_reloid,
-				i_indtable,
-				i_toastheap,
-				i_relfilenumber,
-				i_reltablespace;
-	char		query[QUERY_ALLOC];
-	char	   *last_namespace = NULL,
-			   *last_tablespace = NULL;
+	char	   *query = pg_malloc(QUERY_ALLOC);
 
 	query[0] = '\0';			/* initialize query string to empty */
 
@@ -484,7 +470,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * output, so we have to copy that system table.  It's easiest to do that
 	 * by treating it as a user table.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
+	snprintf(query + strlen(query), QUERY_ALLOC - strlen(query),
 			 "WITH regular_heap (reloid, indtable, toastheap) AS ( "
 			 "  SELECT c.oid, 0::oid, 0::oid "
 			 "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
@@ -506,7 +492,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * selected by the regular_heap CTE.  (We have to do this separately
 	 * because the namespace-name rules above don't work for toast tables.)
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
+	snprintf(query + strlen(query), QUERY_ALLOC - strlen(query),
 			 "  toast_heap (reloid, indtable, toastheap) AS ( "
 			 "  SELECT c.reltoastrelid, 0::oid, c.oid "
 			 "  FROM regular_heap JOIN pg_catalog.pg_class c "
@@ -519,7 +505,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * Testing indisready is necessary in 9.2, and harmless in earlier/later
 	 * versions.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
+	snprintf(query + strlen(query), QUERY_ALLOC - strlen(query),
 			 "  all_index (reloid, indtable, toastheap) AS ( "
 			 "  SELECT indexrelid, indrelid, 0::oid "
 			 "  FROM pg_catalog.pg_index "
@@ -533,7 +519,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * And now we can write the query that retrieves the data we want for each
 	 * heap and index relation.  Make sure result is sorted by OID.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
+	snprintf(query + strlen(query), QUERY_ALLOC - strlen(query),
 			 "SELECT all_rels.*, n.nspname, c.relname, "
 			 "  c.relfilenode, c.reltablespace, "
 			 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
@@ -550,22 +536,30 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 			 "     ON c.reltablespace = t.oid "
 			 "ORDER BY 1;");
 
-	res = executeQueryOrDie(conn, "%s", query);
-
-	ntups = PQntuples(res);
-
-	relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	return query;
+}
 
-	i_reloid = PQfnumber(res, "reloid");
-	i_indtable = PQfnumber(res, "indtable");
-	i_toastheap = PQfnumber(res, "toastheap");
-	i_nspname = PQfnumber(res, "nspname");
-	i_relname = PQfnumber(res, "relname");
-	i_relfilenumber = PQfnumber(res, "relfilenode");
-	i_reltablespace = PQfnumber(res, "reltablespace");
-	i_spclocation = PQfnumber(res, "spclocation");
+static void
+get_rel_infos_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	int			ntups = PQntuples(res);
+	RelInfo    *relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	int			i_reloid = PQfnumber(res, "reloid");
+	int			i_indtable = PQfnumber(res, "indtable");
+	int			i_toastheap = PQfnumber(res, "toastheap");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+	int			i_relfilenumber = PQfnumber(res, "relfilenode");
+	int			i_reltablespace = PQfnumber(res, "reltablespace");
+	int			i_spclocation = PQfnumber(res, "spclocation");
+	int			num_rels = 0;
+	char	   *nspname = NULL;
+	char	   *relname = NULL;
+	char	   *tablespace = NULL;
+	char	   *last_namespace = NULL;
+	char	   *last_tablespace = NULL;
 
-	for (relnum = 0; relnum < ntups; relnum++)
+	for (int relnum = 0; relnum < ntups; relnum++)
 	{
 		RelInfo    *curr = &relinfos[num_rels++];
 
@@ -618,9 +612,6 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 			/* A zero reltablespace oid indicates the database tablespace. */
 			curr->tablespace = dbinfo->db_tablespace;
 	}
-	PQclear(res);
-
-	PQfinish(conn);
 
 	dbinfo->rel_arr.rels = relinfos;
 	dbinfo->rel_arr.nrels = num_rels;
@@ -642,20 +633,9 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
  * check_old_cluster_for_valid_slots() would raise a FATAL error if such slots
  * are included.
  */
-static void
-get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
+static char *
+get_old_cluster_logical_slot_infos_query(DbInfo *dbinfo, void *arg)
 {
-	PGconn	   *conn;
-	PGresult   *res;
-	LogicalSlotInfo *slotinfos = NULL;
-	int			num_slots;
-
-	/* Logical slots can be migrated since PG17. */
-	if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1600)
-		return;
-
-	conn = connectToServer(&old_cluster, dbinfo->db_name);
-
 	/*
 	 * Fetch the logical replication slot information. The check whether the
 	 * slot is considered caught up is done by an upgrade function. This
@@ -673,18 +653,23 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
-							"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
-							"FROM pg_catalog.pg_replication_slots "
-							"WHERE slot_type = 'logical' AND "
-							"database = current_database() AND "
-							"temporary IS FALSE;",
-							user_opts.live_check ? "FALSE" :
-							"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
-							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
-							"END)");
-
-	num_slots = PQntuples(res);
+	return psprintf("SELECT slot_name, plugin, two_phase, failover, "
+					"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
+					"FROM pg_catalog.pg_replication_slots "
+					"WHERE slot_type = 'logical' AND "
+					"database = current_database() AND "
+					"temporary IS FALSE;",
+					user_opts.live_check ? "FALSE" :
+					"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
+					"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
+					"END)");
+}
+
+static void
+get_old_cluster_logical_slot_infos_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	LogicalSlotInfo *slotinfos = NULL;
+	int			num_slots = PQntuples(res);
 
 	if (num_slots)
 	{
@@ -717,14 +702,10 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 		}
 	}
 
-	PQclear(res);
-	PQfinish(conn);
-
 	dbinfo->slot_arr.slots = slotinfos;
 	dbinfo->slot_arr.nslots = num_slots;
 }
 
-
 /*
  * count_old_cluster_logical_slots()
  *
@@ -754,24 +735,18 @@ count_old_cluster_logical_slots(void)
  * This is because before that the logical slots are not upgraded, so we will
  * not be able to upgrade the logical replication clusters completely.
  */
-static void
-get_db_subscription_count(DbInfo *dbinfo)
+static char *
+get_db_subscription_count_query(DbInfo *dbinfo, void *arg)
 {
-	PGconn	   *conn;
-	PGresult   *res;
-
-	/* Subscriptions can be migrated since PG17. */
-	if (GET_MAJOR_VERSION(old_cluster.major_version) < 1700)
-		return;
+	return psprintf("SELECT count(*) "
+					"FROM pg_catalog.pg_subscription WHERE subdbid = %u",
+					dbinfo->db_oid);
+}
 
-	conn = connectToServer(&old_cluster, dbinfo->db_name);
-	res = executeQueryOrDie(conn, "SELECT count(*) "
-							"FROM pg_catalog.pg_subscription WHERE subdbid = %u",
-							dbinfo->db_oid);
+static void
+get_db_subscription_count_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
 	dbinfo->nsubs = atoi(PQgetvalue(res, 0, 0));
-
-	PQclear(res);
-	PQfinish(conn);
 }
 
 /*
-- 
2.39.3 (Apple Git-146)

v3-0005-use-new-pg_upgrade-async-API-to-parallelize-getti.patchtext/plain; charset=us-asciiDownload
From ca28a4f8fb44b1ac6921591ebafab41c6aa7ed98 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:24:35 -0500
Subject: [PATCH v3 5/7] use new pg_upgrade async API to parallelize getting
 loadable libraries

---
 src/bin/pg_upgrade/function.c | 63 ++++++++++++++++++++---------------
 1 file changed, 37 insertions(+), 26 deletions(-)

diff --git a/src/bin/pg_upgrade/function.c b/src/bin/pg_upgrade/function.c
index 7e3abed098..c11fce0696 100644
--- a/src/bin/pg_upgrade/function.c
+++ b/src/bin/pg_upgrade/function.c
@@ -42,6 +42,32 @@ library_name_compare(const void *p1, const void *p2)
 					  ((const LibraryInfo *) p2)->dbnum);
 }
 
+struct loadable_libraries_state
+{
+	PGresult  **ress;
+	int			totaltups;
+};
+
+static char *
+get_loadable_libraries_query(DbInfo *dbinfo, void *arg)
+{
+	return psprintf("SELECT DISTINCT probin "
+					"FROM pg_catalog.pg_proc "
+					"WHERE prolang = %u AND "
+					"probin IS NOT NULL AND "
+					"oid >= %u;",
+					ClanguageId,
+					FirstNormalObjectId);
+}
+
+static void
+get_loadable_libraries_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct loadable_libraries_state *state = (struct loadable_libraries_state *) arg;
+
+	state->ress[dbinfo - old_cluster.dbarr.dbs] = res;
+	state->totaltups += PQntuples(res);
+}
 
 /*
  * get_loadable_libraries()
@@ -54,47 +80,32 @@ library_name_compare(const void *p1, const void *p2)
 void
 get_loadable_libraries(void)
 {
-	PGresult  **ress;
 	int			totaltups;
 	int			dbnum;
 	int			n_libinfos;
+	AsyncTask  *task = async_task_create();
+	struct loadable_libraries_state state;
 
-	ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
-	totaltups = 0;
+	state.ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
+	state.totaltups = 0;
 
-	/* Fetch all library names, removing duplicates within each DB */
-	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
+	async_task_add_step(task, get_loadable_libraries_query,
+						get_loadable_libraries_result, false, &state);
 
-		/*
-		 * Fetch all libraries containing non-built-in C functions in this DB.
-		 */
-		ress[dbnum] = executeQueryOrDie(conn,
-										"SELECT DISTINCT probin "
-										"FROM pg_catalog.pg_proc "
-										"WHERE prolang = %u AND "
-										"probin IS NOT NULL AND "
-										"oid >= %u;",
-										ClanguageId,
-										FirstNormalObjectId);
-		totaltups += PQntuples(ress[dbnum]);
-
-		PQfinish(conn);
-	}
+	async_task_run(task, &old_cluster);
+	async_task_free(task);
 
 	/*
 	 * Allocate memory for required libraries and logical replication output
 	 * plugins.
 	 */
-	n_libinfos = totaltups + count_old_cluster_logical_slots();
+	n_libinfos = state.totaltups + count_old_cluster_logical_slots();
 	os_info.libraries = (LibraryInfo *) pg_malloc(sizeof(LibraryInfo) * n_libinfos);
 	totaltups = 0;
 
 	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
 	{
-		PGresult   *res = ress[dbnum];
+		PGresult   *res = state.ress[dbnum];
 		int			ntups;
 		int			rowno;
 		LogicalSlotInfoArr *slot_arr = &old_cluster.dbarr.dbs[dbnum].slot_arr;
@@ -129,7 +140,7 @@ get_loadable_libraries(void)
 		}
 	}
 
-	pg_free(ress);
+	pg_free(state.ress);
 
 	os_info.num_libraries = totaltups;
 }
-- 
2.39.3 (Apple Git-146)

v3-0006-use-new-pg_upgrade-async-API-to-parallelize-repor.patchtext/plain; charset=us-asciiDownload
From 8ef006b34462a51cf6c52bc1059eb7b38e26147d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:31:57 -0500
Subject: [PATCH v3 6/7] use new pg_upgrade async API to parallelize reporting
 extension updates

---
 src/bin/pg_upgrade/version.c | 82 ++++++++++++++++++------------------
 1 file changed, 41 insertions(+), 41 deletions(-)

diff --git a/src/bin/pg_upgrade/version.c b/src/bin/pg_upgrade/version.c
index 2de6dffccd..12783bb2ba 100644
--- a/src/bin/pg_upgrade/version.c
+++ b/src/bin/pg_upgrade/version.c
@@ -139,6 +139,42 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 		check_ok();
 }
 
+static char *
+report_extension_updates_query(DbInfo *dbinfo, void *arg)
+{
+	return pg_strdup("SELECT name "
+					 "FROM pg_available_extensions "
+					 "WHERE installed_version != default_version");
+}
+
+static void
+report_extension_updates_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_name = PQfnumber(res, "name");
+	char	   *output_path = "update_extensions.sql";
+	FILE	  **script = (FILE **) arg;
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL && (*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			PQExpBufferData connectbuf;
+
+			initPQExpBuffer(&connectbuf);
+			appendPsqlMetaConnect(&connectbuf, dbinfo->db_name);
+			fputs(connectbuf.data, *script);
+			termPQExpBuffer(&connectbuf);
+			db_used = true;
+		}
+		fprintf(*script, "ALTER EXTENSION %s UPDATE;\n",
+				quote_identifier(PQgetvalue(res, rowno, i_name)));
+	}
+}
+
 /*
  * report_extension_updates()
  *	Report extensions that should be updated.
@@ -146,53 +182,17 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 void
 report_extension_updates(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char	   *output_path = "update_extensions.sql";
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for extension updates");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_name;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* find extensions needing updates */
-		res = executeQueryOrDie(conn,
-								"SELECT name "
-								"FROM pg_available_extensions "
-								"WHERE installed_version != default_version"
-			);
-
-		ntups = PQntuples(res);
-		i_name = PQfnumber(res, "name");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				PQExpBufferData connectbuf;
-
-				initPQExpBuffer(&connectbuf);
-				appendPsqlMetaConnect(&connectbuf, active_db->db_name);
-				fputs(connectbuf.data, script);
-				termPQExpBuffer(&connectbuf);
-				db_used = true;
-			}
-			fprintf(script, "ALTER EXTENSION %s UPDATE;\n",
-					quote_identifier(PQgetvalue(res, rowno, i_name)));
-		}
+	async_task_add_step(task, report_extension_updates_query,
+						report_extension_updates_result, true, &script);
 
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v3-0007-parallelize-data-type-checks-in-pg_upgrade.patchtext/plain; charset=us-asciiDownload
From 203af9d3861b4a62548e98e96ee6dad35e7f1123 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Sat, 6 Jul 2024 21:06:31 -0500
Subject: [PATCH v3 7/7] parallelize data type checks in pg_upgrade

---
 src/bin/pg_upgrade/check.c | 319 +++++++++++++++++++------------------
 1 file changed, 160 insertions(+), 159 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 251f3d9017..800d944218 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -32,6 +32,10 @@ static void check_new_cluster_subscription_configuration(void);
 static void check_old_cluster_for_valid_slots(void);
 static void check_old_cluster_subscription_state(void);
 
+static bool *data_type_check_results;
+static bool data_type_check_failed;
+static PQExpBufferData data_type_check_report;
+
 /*
  * DataTypesUsageChecks - definitions of data type checks for the old cluster
  * in order to determine if an upgrade can be performed.  See the comment on
@@ -314,6 +318,129 @@ static DataTypesUsageChecks data_types_usage_checks[] =
 	}
 };
 
+static char *
+data_type_check_query(DbInfo *dbinfo, void *arg)
+{
+	int			i = (int) (intptr_t) arg;
+	DataTypesUsageChecks *check = &data_types_usage_checks[i];
+
+	return psprintf("WITH RECURSIVE oids AS ( "
+	/* start with the type(s) returned by base_query */
+					"	%s "
+					"	UNION ALL "
+					"	SELECT * FROM ( "
+	/* inner WITH because we can only reference the CTE once */
+					"		WITH x AS (SELECT oid FROM oids) "
+	/* domains on any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
+					"			UNION ALL "
+	/* arrays over any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
+					"			UNION ALL "
+	/* composite types containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
+					"			WHERE t.typtype = 'c' AND "
+					"				  t.oid = c.reltype AND "
+					"				  c.oid = a.attrelid AND "
+					"				  NOT a.attisdropped AND "
+					"				  a.atttypid = x.oid "
+					"			UNION ALL "
+	/* ranges containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
+					"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
+					"	) foo "
+					") "
+	/* now look for stored columns of any such type */
+					"SELECT n.nspname, c.relname, a.attname "
+					"FROM	pg_catalog.pg_class c, "
+					"		pg_catalog.pg_namespace n, "
+					"		pg_catalog.pg_attribute a "
+					"WHERE	c.oid = a.attrelid AND "
+					"		NOT a.attisdropped AND "
+					"		a.atttypid IN (SELECT oid FROM oids) AND "
+					"		c.relkind IN ("
+					CppAsString2(RELKIND_RELATION) ", "
+					CppAsString2(RELKIND_MATVIEW) ", "
+					CppAsString2(RELKIND_INDEX) ") AND "
+					"		c.relnamespace = n.oid AND "
+	/* exclude possible orphaned temp tables */
+					"		n.nspname !~ '^pg_temp_' AND "
+					"		n.nspname !~ '^pg_toast_temp_' AND "
+	/* exclude system catalogs, too */
+					"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
+					check->base_query);
+}
+
+static void
+data_type_check_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	int			i = (int) (intptr_t) arg;
+	DataTypesUsageChecks *check = &data_types_usage_checks[i];
+	int			ntups = PQntuples(res);
+
+	if (ntups)
+	{
+		char		output_path[MAXPGPATH];
+		int			i_nspname;
+		int			i_relname;
+		int			i_attname;
+		FILE	   *script = NULL;
+		bool		db_used = false;
+
+		snprintf(output_path, sizeof(output_path), "%s/%s",
+				 log_opts.basedir,
+				 check->report_filename);
+
+		/*
+		 * Make sure we have a buffer to save reports to now that we found a
+		 * first failing check.
+		 */
+		if (!data_type_check_failed)
+			initPQExpBuffer(&data_type_check_report);
+		data_type_check_failed = true;
+
+		/*
+		 * If this is the first time we see an error for the check in question
+		 * then print a status message of the failure.
+		 */
+		if (!data_type_check_results[i])
+		{
+			pg_log(PG_REPORT, "    failed check: %s", _(check->status));
+			appendPQExpBuffer(&data_type_check_report, "\n%s\n%s    %s\n",
+							  _(check->report_text),
+							  _("A list of the problem columns is in the file:"),
+							  output_path);
+		}
+		data_type_check_results[i] = true;
+
+		i_nspname = PQfnumber(res, "nspname");
+		i_relname = PQfnumber(res, "relname");
+		i_attname = PQfnumber(res, "attname");
+
+		for (int rowno = 0; rowno < ntups; rowno++)
+		{
+			if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
+				pg_fatal("could not open file \"%s\": %m", output_path);
+
+			if (!db_used)
+			{
+				fprintf(script, "In database: %s\n", dbinfo->db_name);
+				db_used = true;
+			}
+			fprintf(script, "  %s.%s.%s\n",
+					PQgetvalue(res, rowno, i_nspname),
+					PQgetvalue(res, rowno, i_relname),
+					PQgetvalue(res, rowno, i_attname));
+		}
+
+		if (script)
+		{
+			fclose(script);
+			script = NULL;
+		}
+	}
+}
+
 /*
  * check_for_data_types_usage()
  *	Detect whether there are any stored columns depending on given type(s)
@@ -337,10 +464,9 @@ static void
 check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
 {
 	bool		found = false;
-	bool	   *results;
-	PQExpBufferData report;
 	DataTypesUsageChecks *tmp = checks;
 	int			n_data_types_usage_checks = 0;
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for data type usage");
 
@@ -352,176 +478,51 @@ check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
 	}
 
 	/* Prepare an array to store the results of checks in */
-	results = pg_malloc0(sizeof(bool) * n_data_types_usage_checks);
+	data_type_check_results = pg_malloc0(sizeof(bool) * n_data_types_usage_checks);
+	data_type_check_failed = false;
 
-	/*
-	 * Connect to each database in the cluster and run all defined checks
-	 * against that database before trying the next one.
-	 */
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int i = 0; i < n_data_types_usage_checks; i++)
 	{
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
+		DataTypesUsageChecks *check = &checks[i];
 
-		for (int checknum = 0; checknum < n_data_types_usage_checks; checknum++)
+		if (check->threshold_version == MANUAL_CHECK)
 		{
-			PGresult   *res;
-			int			ntups;
-			int			i_nspname;
-			int			i_relname;
-			int			i_attname;
-			FILE	   *script = NULL;
-			bool		db_used = false;
-			char		output_path[MAXPGPATH];
-			DataTypesUsageChecks *cur_check = &checks[checknum];
-
-			if (cur_check->threshold_version == MANUAL_CHECK)
-			{
-				Assert(cur_check->version_hook);
-
-				/*
-				 * Make sure that the check applies to the current cluster
-				 * version and skip if not. If no check hook has been defined
-				 * we run the check for all versions.
-				 */
-				if (!cur_check->version_hook(cluster))
-					continue;
-			}
-			else if (cur_check->threshold_version != ALL_VERSIONS)
-			{
-				if (GET_MAJOR_VERSION(cluster->major_version) > cur_check->threshold_version)
-					continue;
-			}
-			else
-				Assert(cur_check->threshold_version == ALL_VERSIONS);
-
-			snprintf(output_path, sizeof(output_path), "%s/%s",
-					 log_opts.basedir,
-					 cur_check->report_filename);
-
-			/*
-			 * The type(s) of interest might be wrapped in a domain, array,
-			 * composite, or range, and these container types can be nested
-			 * (to varying extents depending on server version, but that's not
-			 * of concern here).  To handle all these cases we need a
-			 * recursive CTE.
-			 */
-			res = executeQueryOrDie(conn,
-									"WITH RECURSIVE oids AS ( "
-			/* start with the type(s) returned by base_query */
-									"	%s "
-									"	UNION ALL "
-									"	SELECT * FROM ( "
-			/* inner WITH because we can only reference the CTE once */
-									"		WITH x AS (SELECT oid FROM oids) "
-			/* domains on any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
-									"			UNION ALL "
-			/* arrays over any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
-									"			UNION ALL "
-			/* composite types containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
-									"			WHERE t.typtype = 'c' AND "
-									"				  t.oid = c.reltype AND "
-									"				  c.oid = a.attrelid AND "
-									"				  NOT a.attisdropped AND "
-									"				  a.atttypid = x.oid "
-									"			UNION ALL "
-			/* ranges containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
-									"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
-									"	) foo "
-									") "
-			/* now look for stored columns of any such type */
-									"SELECT n.nspname, c.relname, a.attname "
-									"FROM	pg_catalog.pg_class c, "
-									"		pg_catalog.pg_namespace n, "
-									"		pg_catalog.pg_attribute a "
-									"WHERE	c.oid = a.attrelid AND "
-									"		NOT a.attisdropped AND "
-									"		a.atttypid IN (SELECT oid FROM oids) AND "
-									"		c.relkind IN ("
-									CppAsString2(RELKIND_RELATION) ", "
-									CppAsString2(RELKIND_MATVIEW) ", "
-									CppAsString2(RELKIND_INDEX) ") AND "
-									"		c.relnamespace = n.oid AND "
-			/* exclude possible orphaned temp tables */
-									"		n.nspname !~ '^pg_temp_' AND "
-									"		n.nspname !~ '^pg_toast_temp_' AND "
-			/* exclude system catalogs, too */
-									"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
-									cur_check->base_query);
-
-			ntups = PQntuples(res);
+			Assert(check->version_hook);
 
 			/*
-			 * The datatype was found, so extract the data and log to the
-			 * requested filename. We need to open the file for appending
-			 * since the check might have already found the type in another
-			 * database earlier in the loop.
+			 * Make sure that the check applies to the current cluster version
+			 * and skip it if not.
 			 */
-			if (ntups)
-			{
-				/*
-				 * Make sure we have a buffer to save reports to now that we
-				 * found a first failing check.
-				 */
-				if (!found)
-					initPQExpBuffer(&report);
-				found = true;
-
-				/*
-				 * If this is the first time we see an error for the check in
-				 * question then print a status message of the failure.
-				 */
-				if (!results[checknum])
-				{
-					pg_log(PG_REPORT, "    failed check: %s", _(cur_check->status));
-					appendPQExpBuffer(&report, "\n%s\n%s    %s\n",
-									  _(cur_check->report_text),
-									  _("A list of the problem columns is in the file:"),
-									  output_path);
-				}
-				results[checknum] = true;
-
-				i_nspname = PQfnumber(res, "nspname");
-				i_relname = PQfnumber(res, "relname");
-				i_attname = PQfnumber(res, "attname");
-
-				for (int rowno = 0; rowno < ntups; rowno++)
-				{
-					if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
-						pg_fatal("could not open file \"%s\": %m", output_path);
-
-					if (!db_used)
-					{
-						fprintf(script, "In database: %s\n", active_db->db_name);
-						db_used = true;
-					}
-					fprintf(script, "  %s.%s.%s\n",
-							PQgetvalue(res, rowno, i_nspname),
-							PQgetvalue(res, rowno, i_relname),
-							PQgetvalue(res, rowno, i_attname));
-				}
-
-				if (script)
-				{
-					fclose(script);
-					script = NULL;
-				}
-			}
-
-			PQclear(res);
+			if (!check->version_hook(cluster))
+				continue;
 		}
+		else if (check->threshold_version != ALL_VERSIONS)
+		{
+			if (GET_MAJOR_VERSION(cluster->major_version) > check->threshold_version)
+				continue;
+		}
+		else
+			Assert(check->threshold_version == ALL_VERSIONS);
 
-		PQfinish(conn);
+		async_task_add_step(task, data_type_check_query,
+							data_type_check_process, true,
+							(void *) (intptr_t) i);
 	}
 
+	/*
+	 * Connect to each database in the cluster and run all defined checks
+	 * against that database before trying the next one.
+	 */
+	async_task_run(task, cluster);
+	async_task_free(task);
+
 	if (found)
-		pg_fatal("Data type checks failed: %s", report.data);
+	{
+		pg_fatal("Data type checks failed: %s", data_type_check_report.data);
+		termPQExpBuffer(&data_type_check_report);
+	}
 
-	pg_free(results);
+	pg_free(data_type_check_results);
 
 	check_ok();
 }
-- 
2.39.3 (Apple Git-146)

#8Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#7)
12 attachment(s)
Re: optimizing pg_upgrade's once-in-each-database steps

I finished parallelizing everything in pg_upgrade I could easily
parallelize with the proposed async task API. There are a few remaining
places where I could not easily convert to the new API for whatever reason.
AFAICT those remaining places are either not showing up prominently in my
tests, or they only affect versions that are unsupported or will soon be
unsupported when v18 is released. Similarly, I noticed many of the checks
follow a very similar coding pattern, but most (if not all) only affect
older versions, so I'm unsure whether it's worth the trouble to try
consolidating them into one scan through all the databases.

The code is still very rough and nowhere near committable, but this at
least gets the patch set into the editing phase.

--
nathan

Attachments:

v4-0002-use-new-pg_upgrade-async-API-for-subscription-sta.patchtext/plain; charset=us-asciiDownload
From 7997a6c15c6b348fb0c0941ddf99c90364cfb21c Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 17:21:19 -0500
Subject: [PATCH v4 02/12] use new pg_upgrade async API for subscription state
 checks

---
 src/bin/pg_upgrade/check.c | 200 ++++++++++++++++++++-----------------
 1 file changed, 106 insertions(+), 94 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 27924159d6..f653fa25a5 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1906,6 +1906,75 @@ check_old_cluster_for_valid_slots(bool live_check)
 	check_ok();
 }
 
+/* private state for subscription state checks */
+struct substate_info
+{
+	FILE	   *script;
+	char		output_path[MAXPGPATH];
+};
+
+/*
+ * We don't allow upgrade if there is a risk of dangling slot or origin
+ * corresponding to initial sync after upgrade.
+ *
+ * A slot/origin not created yet refers to the 'i' (initialize) state, while
+ * 'r' (ready) state refers to a slot/origin created previously but already
+ * dropped. These states are supported for pg_upgrade. The other states listed
+ * below are not supported:
+ *
+ * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state would
+ * retain a replication slot, which could not be dropped by the sync worker
+ * spawned after the upgrade because the subscription ID used for the slot name
+ * won't match anymore.
+ *
+ * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state would
+ * retain the replication origin when there is a failure in tablesync worker
+ * immediately after dropping the replication slot in the publisher.
+ *
+ * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on a
+ * relation upgraded while in this state would expect an origin ID with the OID
+ * of the subscription used before the upgrade, causing it to fail.
+ *
+ * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and SUBREL_STATE_UNKNOWN:
+ * These states are not stored in the catalog, so we need not allow these
+ * states.
+ */
+static char *
+sub_query(DbInfo *dbinfo, void *arg)
+{
+	return pg_strdup("SELECT r.srsubstate, s.subname, n.nspname, c.relname "
+					 "FROM pg_catalog.pg_subscription_rel r "
+					 "LEFT JOIN pg_catalog.pg_subscription s"
+					 "   ON r.srsubid = s.oid "
+					 "LEFT JOIN pg_catalog.pg_class c"
+					 "   ON r.srrelid = c.oid "
+					 "LEFT JOIN pg_catalog.pg_namespace n"
+					 "   ON c.relnamespace = n.oid "
+					 "WHERE r.srsubstate NOT IN ('i', 'r') "
+					 "ORDER BY s.subname");
+}
+
+static void
+sub_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct substate_info *state = (struct substate_info *) arg;
+	int			ntup = PQntuples(res);
+
+	for (int i = 0; i < ntup; i++)
+	{
+		if (state->script == NULL &&
+			(state->script = fopen_priv(state->output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state->output_path);
+
+		fprintf(state->script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
+				PQgetvalue(res, i, 0),
+				dbinfo->db_name,
+				PQgetvalue(res, i, 1),
+				PQgetvalue(res, i, 2),
+				PQgetvalue(res, i, 3));
+	}
+}
+
 /*
  * check_old_cluster_subscription_state()
  *
@@ -1916,115 +1985,58 @@ check_old_cluster_for_valid_slots(bool live_check)
 static void
 check_old_cluster_subscription_state(void)
 {
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
+	struct substate_info state;
+	PGresult   *res;
+	PGconn	   *conn;
 	int			ntup;
 
 	prep_status("Checking for subscription state");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	state.script = NULL;
+	snprintf(state.output_path, sizeof(state.output_path), "%s/%s",
 			 log_opts.basedir,
 			 "subs_invalid.txt");
-	for (int dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
 
-		/* We need to check for pg_replication_origin only once. */
-		if (dbnum == 0)
-		{
-			/*
-			 * Check that all the subscriptions have their respective
-			 * replication origin.
-			 */
-			res = executeQueryOrDie(conn,
-									"SELECT d.datname, s.subname "
-									"FROM pg_catalog.pg_subscription s "
-									"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
-									"	ON o.roname = 'pg_' || s.oid "
-									"INNER JOIN pg_catalog.pg_database d "
-									"	ON d.oid = s.subdbid "
-									"WHERE o.roname IS NULL;");
-
-			ntup = PQntuples(res);
-			for (int i = 0; i < ntup; i++)
-			{
-				if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-					pg_fatal("could not open file \"%s\": %m", output_path);
-				fprintf(script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
-						PQgetvalue(res, i, 0),
-						PQgetvalue(res, i, 1));
-			}
-			PQclear(res);
-		}
-
-		/*
-		 * We don't allow upgrade if there is a risk of dangling slot or
-		 * origin corresponding to initial sync after upgrade.
-		 *
-		 * A slot/origin not created yet refers to the 'i' (initialize) state,
-		 * while 'r' (ready) state refers to a slot/origin created previously
-		 * but already dropped. These states are supported for pg_upgrade. The
-		 * other states listed below are not supported:
-		 *
-		 * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state
-		 * would retain a replication slot, which could not be dropped by the
-		 * sync worker spawned after the upgrade because the subscription ID
-		 * used for the slot name won't match anymore.
-		 *
-		 * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state
-		 * would retain the replication origin when there is a failure in
-		 * tablesync worker immediately after dropping the replication slot in
-		 * the publisher.
-		 *
-		 * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on
-		 * a relation upgraded while in this state would expect an origin ID
-		 * with the OID of the subscription used before the upgrade, causing
-		 * it to fail.
-		 *
-		 * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and
-		 * SUBREL_STATE_UNKNOWN: These states are not stored in the catalog,
-		 * so we need not allow these states.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT r.srsubstate, s.subname, n.nspname, c.relname "
-								"FROM pg_catalog.pg_subscription_rel r "
-								"LEFT JOIN pg_catalog.pg_subscription s"
-								"	ON r.srsubid = s.oid "
-								"LEFT JOIN pg_catalog.pg_class c"
-								"	ON r.srrelid = c.oid "
-								"LEFT JOIN pg_catalog.pg_namespace n"
-								"	ON c.relnamespace = n.oid "
-								"WHERE r.srsubstate NOT IN ('i', 'r') "
-								"ORDER BY s.subname");
-
-		ntup = PQntuples(res);
-		for (int i = 0; i < ntup; i++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
+	/*
+	 * Check that all the subscriptions have their respective replication
+	 * origin.  This check only needs to run once.
+	 */
+	conn = connectToServer(&old_cluster, old_cluster.dbarr.dbs[0].db_name);
+	res = executeQueryOrDie(conn,
+							"SELECT d.datname, s.subname "
+							"FROM pg_catalog.pg_subscription s "
+							"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
+							"	ON o.roname = 'pg_' || s.oid "
+							"INNER JOIN pg_catalog.pg_database d "
+							"	ON d.oid = s.subdbid "
+							"WHERE o.roname IS NULL;");
+	ntup = PQntuples(res);
+	for (int i = 0; i < ntup; i++)
+	{
+		if (state.script == NULL &&
+			(state.script = fopen_priv(state.output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state.output_path);
+		fprintf(state.script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
+				PQgetvalue(res, i, 0),
+				PQgetvalue(res, i, 1));
+	}
+	PQclear(res);
+	PQfinish(conn);
 
-			fprintf(script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
-					PQgetvalue(res, i, 0),
-					active_db->db_name,
-					PQgetvalue(res, i, 1),
-					PQgetvalue(res, i, 2),
-					PQgetvalue(res, i, 3));
-		}
+	async_task_add_step(task, sub_query, sub_process, true, &state);
 
-		PQclear(res);
-		PQfinish(conn);
-	}
+	async_task_run(task, &old_cluster);
+	async_task_free(task);
 
-	if (script)
+	if (state.script)
 	{
-		fclose(script);
+		fclose(state.script);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains subscriptions without origin or having relations not in i (initialize) or r (ready) state.\n"
 				 "You can allow the initial sync to finish for all relations and then restart the upgrade.\n"
 				 "A list of the problematic subscriptions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", state.output_path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v4-0003-move-live_check-variable-to-user_opts.patchtext/plain; charset=us-asciiDownload
From 9616fb82ef2a77a4c13b943118bb776d0f1a5a1c Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 21 May 2024 16:35:19 -0500
Subject: [PATCH v4 03/12] move live_check variable to user_opts

---
 src/bin/pg_upgrade/check.c       | 32 ++++++++++++++++----------------
 src/bin/pg_upgrade/controldata.c |  5 +++--
 src/bin/pg_upgrade/info.c        | 12 +++++-------
 src/bin/pg_upgrade/option.c      |  4 ++--
 src/bin/pg_upgrade/pg_upgrade.c  | 21 ++++++++++-----------
 src/bin/pg_upgrade/pg_upgrade.h  | 13 +++++++------
 6 files changed, 43 insertions(+), 44 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index f653fa25a5..251f3d9017 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -29,7 +29,7 @@ static void check_for_new_tablespace_dir(void);
 static void check_for_user_defined_encoding_conversions(ClusterInfo *cluster);
 static void check_new_cluster_logical_replication_slots(void);
 static void check_new_cluster_subscription_configuration(void);
-static void check_old_cluster_for_valid_slots(bool live_check);
+static void check_old_cluster_for_valid_slots(void);
 static void check_old_cluster_subscription_state(void);
 
 /*
@@ -555,9 +555,9 @@ fix_path_separator(char *path)
 }
 
 void
-output_check_banner(bool live_check)
+output_check_banner(void)
 {
-	if (user_opts.check && live_check)
+	if (user_opts.live_check)
 	{
 		pg_log(PG_REPORT,
 			   "Performing Consistency Checks on Old Live Server\n"
@@ -573,18 +573,18 @@ output_check_banner(bool live_check)
 
 
 void
-check_and_dump_old_cluster(bool live_check)
+check_and_dump_old_cluster(void)
 {
 	/* -- OLD -- */
 
-	if (!live_check)
+	if (!user_opts.live_check)
 		start_postmaster(&old_cluster, true);
 
 	/*
 	 * Extract a list of databases, tables, and logical replication slots from
 	 * the old cluster.
 	 */
-	get_db_rel_and_slot_infos(&old_cluster, live_check);
+	get_db_rel_and_slot_infos(&old_cluster);
 
 	init_tablespaces();
 
@@ -605,7 +605,7 @@ check_and_dump_old_cluster(bool live_check)
 		 * Logical replication slots can be migrated since PG17. See comments
 		 * atop get_old_cluster_logical_slot_infos().
 		 */
-		check_old_cluster_for_valid_slots(live_check);
+		check_old_cluster_for_valid_slots();
 
 		/*
 		 * Subscriptions and their dependencies can be migrated since PG17.
@@ -652,7 +652,7 @@ check_and_dump_old_cluster(bool live_check)
 	 */
 	if (GET_MAJOR_VERSION(old_cluster.major_version) <= 906)
 	{
-		if (user_opts.check)
+		if (user_opts.live_check)
 			old_9_6_invalidate_hash_indexes(&old_cluster, true);
 	}
 
@@ -667,7 +667,7 @@ check_and_dump_old_cluster(bool live_check)
 	if (!user_opts.check)
 		generate_old_dump();
 
-	if (!live_check)
+	if (!user_opts.live_check)
 		stop_postmaster(false);
 }
 
@@ -675,7 +675,7 @@ check_and_dump_old_cluster(bool live_check)
 void
 check_new_cluster(void)
 {
-	get_db_rel_and_slot_infos(&new_cluster, false);
+	get_db_rel_and_slot_infos(&new_cluster);
 
 	check_new_cluster_is_empty();
 
@@ -826,14 +826,14 @@ check_cluster_versions(void)
 
 
 void
-check_cluster_compatibility(bool live_check)
+check_cluster_compatibility(void)
 {
 	/* get/check pg_control data of servers */
-	get_control_data(&old_cluster, live_check);
-	get_control_data(&new_cluster, false);
+	get_control_data(&old_cluster);
+	get_control_data(&new_cluster);
 	check_control_data(&old_cluster.controldata, &new_cluster.controldata);
 
-	if (live_check && old_cluster.port == new_cluster.port)
+	if (user_opts.live_check && old_cluster.port == new_cluster.port)
 		pg_fatal("When checking a live server, "
 				 "the old and new port numbers must be different.");
 }
@@ -1839,7 +1839,7 @@ check_new_cluster_subscription_configuration(void)
  * before shutdown.
  */
 static void
-check_old_cluster_for_valid_slots(bool live_check)
+check_old_cluster_for_valid_slots(void)
 {
 	char		output_path[MAXPGPATH];
 	FILE	   *script = NULL;
@@ -1878,7 +1878,7 @@ check_old_cluster_for_valid_slots(bool live_check)
 			 * Note: This can be satisfied only when the old cluster has been
 			 * shut down, so we skip this for live checks.
 			 */
-			if (!live_check && !slot->caught_up)
+			if (!user_opts.live_check && !slot->caught_up)
 			{
 				if (script == NULL &&
 					(script = fopen_priv(output_path, "w")) == NULL)
diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c
index 1f0ccea3ed..cf665b9dee 100644
--- a/src/bin/pg_upgrade/controldata.c
+++ b/src/bin/pg_upgrade/controldata.c
@@ -33,7 +33,7 @@
  * return valid xid data for a running server.
  */
 void
-get_control_data(ClusterInfo *cluster, bool live_check)
+get_control_data(ClusterInfo *cluster)
 {
 	char		cmd[MAXPGPATH];
 	char		bufin[MAX_STRING];
@@ -76,6 +76,7 @@ get_control_data(ClusterInfo *cluster, bool live_check)
 	uint32		segno = 0;
 	char	   *resetwal_bin;
 	int			rc;
+	bool		live_check = (cluster == &old_cluster && user_opts.live_check);
 
 	/*
 	 * Because we test the pg_resetwal output as strings, it has to be in
@@ -118,7 +119,7 @@ get_control_data(ClusterInfo *cluster, bool live_check)
 	/*
 	 * Check for clean shutdown
 	 */
-	if (!live_check || cluster == &new_cluster)
+	if (!live_check)
 	{
 		/* only pg_controldata outputs the cluster state */
 		snprintf(cmd, sizeof(cmd), "\"%s/pg_controldata\" \"%s\"",
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 95c22a7200..8f1777de59 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -27,7 +27,7 @@ static void free_rel_infos(RelInfoArr *rel_arr);
 static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
-static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check);
+static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo);
 static void get_db_subscription_count(DbInfo *dbinfo);
 
 
@@ -273,11 +273,9 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
  *
  * higher level routine to generate dbinfos for the database running
  * on the given "port". Assumes that server is already running.
- *
- * live_check would be used only when the target is the old cluster.
  */
 void
-get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check)
+get_db_rel_and_slot_infos(ClusterInfo *cluster)
 {
 	int			dbnum;
 
@@ -299,7 +297,7 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check)
 		 */
 		if (cluster == &old_cluster)
 		{
-			get_old_cluster_logical_slot_infos(pDbInfo, live_check);
+			get_old_cluster_logical_slot_infos(pDbInfo);
 			get_db_subscription_count(pDbInfo);
 		}
 	}
@@ -645,7 +643,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
  * are included.
  */
 static void
-get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
+get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -681,7 +679,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 							"WHERE slot_type = 'logical' AND "
 							"database = current_database() AND "
 							"temporary IS FALSE;",
-							live_check ? "FALSE" :
+							user_opts.live_check ? "FALSE" :
 							"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
 							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
 							"END)");
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index 548ea4e623..6f41d63eed 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -470,10 +470,10 @@ adjust_data_dir(ClusterInfo *cluster)
  * directory.
  */
 void
-get_sock_dir(ClusterInfo *cluster, bool live_check)
+get_sock_dir(ClusterInfo *cluster)
 {
 #if !defined(WIN32)
-	if (!live_check)
+	if (!user_opts.live_check || cluster == &new_cluster)
 		cluster->sockdir = user_opts.socketdir;
 	else
 	{
diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c
index 03eb738fd7..99f3d4543e 100644
--- a/src/bin/pg_upgrade/pg_upgrade.c
+++ b/src/bin/pg_upgrade/pg_upgrade.c
@@ -65,7 +65,7 @@ static void create_new_objects(void);
 static void copy_xact_xlog_xid(void);
 static void set_frozenxids(bool minmxid_only);
 static void make_outputdirs(char *pgdata);
-static void setup(char *argv0, bool *live_check);
+static void setup(char *argv0);
 static void create_logical_replication_slots(void);
 
 ClusterInfo old_cluster,
@@ -88,7 +88,6 @@ int
 main(int argc, char **argv)
 {
 	char	   *deletion_script_file_name = NULL;
-	bool		live_check = false;
 
 	/*
 	 * pg_upgrade doesn't currently use common/logging.c, but initialize it
@@ -123,18 +122,18 @@ main(int argc, char **argv)
 	 */
 	make_outputdirs(new_cluster.pgdata);
 
-	setup(argv[0], &live_check);
+	setup(argv[0]);
 
-	output_check_banner(live_check);
+	output_check_banner();
 
 	check_cluster_versions();
 
-	get_sock_dir(&old_cluster, live_check);
-	get_sock_dir(&new_cluster, false);
+	get_sock_dir(&old_cluster);
+	get_sock_dir(&new_cluster);
 
-	check_cluster_compatibility(live_check);
+	check_cluster_compatibility();
 
-	check_and_dump_old_cluster(live_check);
+	check_and_dump_old_cluster();
 
 
 	/* -- NEW -- */
@@ -331,7 +330,7 @@ make_outputdirs(char *pgdata)
 
 
 static void
-setup(char *argv0, bool *live_check)
+setup(char *argv0)
 {
 	/*
 	 * make sure the user has a clean environment, otherwise, we may confuse
@@ -378,7 +377,7 @@ setup(char *argv0, bool *live_check)
 				pg_fatal("There seems to be a postmaster servicing the old cluster.\n"
 						 "Please shutdown that postmaster and try again.");
 			else
-				*live_check = true;
+				user_opts.live_check = true;
 		}
 	}
 
@@ -660,7 +659,7 @@ create_new_objects(void)
 		set_frozenxids(true);
 
 	/* update new_cluster info now that we have objects in the databases */
-	get_db_rel_and_slot_infos(&new_cluster, false);
+	get_db_rel_and_slot_infos(&new_cluster);
 }
 
 /*
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 1ebad3bd74..56d05d7eb9 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -322,6 +322,7 @@ typedef struct
 typedef struct
 {
 	bool		check;			/* check clusters only, don't change any data */
+	bool		live_check;		/* check clusters only, old server is running */
 	bool		do_sync;		/* flush changes to disk */
 	transferMode transfer_mode; /* copy files or link them? */
 	int			jobs;			/* number of processes/threads to use */
@@ -366,20 +367,20 @@ extern OSInfo os_info;
 
 /* check.c */
 
-void		output_check_banner(bool live_check);
-void		check_and_dump_old_cluster(bool live_check);
+void		output_check_banner(void);
+void		check_and_dump_old_cluster(void);
 void		check_new_cluster(void);
 void		report_clusters_compatible(void);
 void		issue_warnings_and_set_wal_level(void);
 void		output_completion_banner(char *deletion_script_file_name);
 void		check_cluster_versions(void);
-void		check_cluster_compatibility(bool live_check);
+void		check_cluster_compatibility(void);
 void		create_script_for_old_cluster_deletion(char **deletion_script_file_name);
 
 
 /* controldata.c */
 
-void		get_control_data(ClusterInfo *cluster, bool live_check);
+void		get_control_data(ClusterInfo *cluster);
 void		check_control_data(ControlData *oldctrl, ControlData *newctrl);
 void		disable_old_cluster(void);
 
@@ -428,7 +429,7 @@ void		check_loadable_libraries(void);
 FileNameMap *gen_db_file_maps(DbInfo *old_db,
 							  DbInfo *new_db, int *nmaps, const char *old_pgdata,
 							  const char *new_pgdata);
-void		get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check);
+void		get_db_rel_and_slot_infos(ClusterInfo *cluster);
 int			count_old_cluster_logical_slots(void);
 int			count_old_cluster_subscriptions(void);
 
@@ -436,7 +437,7 @@ int			count_old_cluster_subscriptions(void);
 
 void		parseCommandLine(int argc, char *argv[]);
 void		adjust_data_dir(ClusterInfo *cluster);
-void		get_sock_dir(ClusterInfo *cluster, bool live_check);
+void		get_sock_dir(ClusterInfo *cluster);
 
 /* relfilenumber.c */
 
-- 
2.39.3 (Apple Git-146)

v4-0004-use-new-pg_upgrade-async-API-for-retrieving-relin.patchtext/plain; charset=us-asciiDownload
From 38a7219a1170359c23393fcb896bd446d5062dab Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:09:33 -0500
Subject: [PATCH v4 04/12] use new pg_upgrade async API for retrieving relinfos

---
 src/bin/pg_upgrade/info.c | 187 +++++++++++++++++---------------------
 1 file changed, 81 insertions(+), 106 deletions(-)

diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 8f1777de59..d07255bd0a 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -22,13 +22,16 @@ static void report_unmatched_relation(const RelInfo *rel, const DbInfo *db,
 static void free_db_and_rel_infos(DbInfoArr *db_arr);
 static void get_template0_info(ClusterInfo *cluster);
 static void get_db_infos(ClusterInfo *cluster);
-static void get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo);
+static char *get_rel_infos_query(DbInfo *dbinfo, void *arg);
+static void get_rel_infos_result(DbInfo *dbinfo, PGresult *res, void *arg);
 static void free_rel_infos(RelInfoArr *rel_arr);
 static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
-static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo);
-static void get_db_subscription_count(DbInfo *dbinfo);
+static char *get_old_cluster_logical_slot_infos_query(DbInfo *dbinfo, void *arg);
+static void get_old_cluster_logical_slot_infos_result(DbInfo *dbinfo, PGresult *res, void *arg);
+static char *get_db_subscription_count_query(DbInfo *dbinfo, void *arg);
+static void get_db_subscription_count_result(DbInfo *dbinfo, PGresult *res, void *arg);
 
 
 /*
@@ -277,7 +280,7 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
 void
 get_db_rel_and_slot_infos(ClusterInfo *cluster)
 {
-	int			dbnum;
+	AsyncTask  *task = async_task_create();
 
 	if (cluster->dbarr.dbs != NULL)
 		free_db_and_rel_infos(&cluster->dbarr);
@@ -285,23 +288,26 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster)
 	get_template0_info(cluster);
 	get_db_infos(cluster);
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	async_task_add_step(task,
+						get_rel_infos_query,
+						get_rel_infos_result,
+						true, NULL);
+	if (cluster == &old_cluster &&
+		GET_MAJOR_VERSION(cluster->major_version) > 1600)
 	{
-		DbInfo	   *pDbInfo = &cluster->dbarr.dbs[dbnum];
-
-		get_rel_infos(cluster, pDbInfo);
-
-		/*
-		 * Retrieve the logical replication slots infos and the subscriptions
-		 * count for the old cluster.
-		 */
-		if (cluster == &old_cluster)
-		{
-			get_old_cluster_logical_slot_infos(pDbInfo);
-			get_db_subscription_count(pDbInfo);
-		}
+		async_task_add_step(task,
+							get_old_cluster_logical_slot_infos_query,
+							get_old_cluster_logical_slot_infos_result,
+							true, cluster);
+		async_task_add_step(task,
+							get_db_subscription_count_query,
+							get_db_subscription_count_result,
+							true, cluster);
 	}
 
+	async_task_run(task, cluster);
+	async_task_free(task);
+
 	if (cluster == &old_cluster)
 		pg_log(PG_VERBOSE, "\nsource databases:");
 	else
@@ -447,30 +453,10 @@ get_db_infos(ClusterInfo *cluster)
  * Note: the resulting RelInfo array is assumed to be sorted by OID.
  * This allows later processing to match up old and new databases efficiently.
  */
-static void
-get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
+static char *
+get_rel_infos_query(DbInfo *dbinfo, void *arg)
 {
-	PGconn	   *conn = connectToServer(cluster,
-									   dbinfo->db_name);
-	PGresult   *res;
-	RelInfo    *relinfos;
-	int			ntups;
-	int			relnum;
-	int			num_rels = 0;
-	char	   *nspname = NULL;
-	char	   *relname = NULL;
-	char	   *tablespace = NULL;
-	int			i_spclocation,
-				i_nspname,
-				i_relname,
-				i_reloid,
-				i_indtable,
-				i_toastheap,
-				i_relfilenumber,
-				i_reltablespace;
-	char		query[QUERY_ALLOC];
-	char	   *last_namespace = NULL,
-			   *last_tablespace = NULL;
+	char	   *query = pg_malloc(QUERY_ALLOC);
 
 	query[0] = '\0';			/* initialize query string to empty */
 
@@ -484,7 +470,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * output, so we have to copy that system table.  It's easiest to do that
 	 * by treating it as a user table.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
+	snprintf(query + strlen(query), QUERY_ALLOC - strlen(query),
 			 "WITH regular_heap (reloid, indtable, toastheap) AS ( "
 			 "  SELECT c.oid, 0::oid, 0::oid "
 			 "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
@@ -506,7 +492,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * selected by the regular_heap CTE.  (We have to do this separately
 	 * because the namespace-name rules above don't work for toast tables.)
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
+	snprintf(query + strlen(query), QUERY_ALLOC - strlen(query),
 			 "  toast_heap (reloid, indtable, toastheap) AS ( "
 			 "  SELECT c.reltoastrelid, 0::oid, c.oid "
 			 "  FROM regular_heap JOIN pg_catalog.pg_class c "
@@ -519,7 +505,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * Testing indisready is necessary in 9.2, and harmless in earlier/later
 	 * versions.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
+	snprintf(query + strlen(query), QUERY_ALLOC - strlen(query),
 			 "  all_index (reloid, indtable, toastheap) AS ( "
 			 "  SELECT indexrelid, indrelid, 0::oid "
 			 "  FROM pg_catalog.pg_index "
@@ -533,7 +519,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * And now we can write the query that retrieves the data we want for each
 	 * heap and index relation.  Make sure result is sorted by OID.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
+	snprintf(query + strlen(query), QUERY_ALLOC - strlen(query),
 			 "SELECT all_rels.*, n.nspname, c.relname, "
 			 "  c.relfilenode, c.reltablespace, "
 			 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
@@ -550,22 +536,30 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 			 "     ON c.reltablespace = t.oid "
 			 "ORDER BY 1;");
 
-	res = executeQueryOrDie(conn, "%s", query);
-
-	ntups = PQntuples(res);
-
-	relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	return query;
+}
 
-	i_reloid = PQfnumber(res, "reloid");
-	i_indtable = PQfnumber(res, "indtable");
-	i_toastheap = PQfnumber(res, "toastheap");
-	i_nspname = PQfnumber(res, "nspname");
-	i_relname = PQfnumber(res, "relname");
-	i_relfilenumber = PQfnumber(res, "relfilenode");
-	i_reltablespace = PQfnumber(res, "reltablespace");
-	i_spclocation = PQfnumber(res, "spclocation");
+static void
+get_rel_infos_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	int			ntups = PQntuples(res);
+	RelInfo    *relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	int			i_reloid = PQfnumber(res, "reloid");
+	int			i_indtable = PQfnumber(res, "indtable");
+	int			i_toastheap = PQfnumber(res, "toastheap");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+	int			i_relfilenumber = PQfnumber(res, "relfilenode");
+	int			i_reltablespace = PQfnumber(res, "reltablespace");
+	int			i_spclocation = PQfnumber(res, "spclocation");
+	int			num_rels = 0;
+	char	   *nspname = NULL;
+	char	   *relname = NULL;
+	char	   *tablespace = NULL;
+	char	   *last_namespace = NULL;
+	char	   *last_tablespace = NULL;
 
-	for (relnum = 0; relnum < ntups; relnum++)
+	for (int relnum = 0; relnum < ntups; relnum++)
 	{
 		RelInfo    *curr = &relinfos[num_rels++];
 
@@ -618,9 +612,6 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 			/* A zero reltablespace oid indicates the database tablespace. */
 			curr->tablespace = dbinfo->db_tablespace;
 	}
-	PQclear(res);
-
-	PQfinish(conn);
 
 	dbinfo->rel_arr.rels = relinfos;
 	dbinfo->rel_arr.nrels = num_rels;
@@ -642,20 +633,9 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
  * check_old_cluster_for_valid_slots() would raise a FATAL error if such slots
  * are included.
  */
-static void
-get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
+static char *
+get_old_cluster_logical_slot_infos_query(DbInfo *dbinfo, void *arg)
 {
-	PGconn	   *conn;
-	PGresult   *res;
-	LogicalSlotInfo *slotinfos = NULL;
-	int			num_slots;
-
-	/* Logical slots can be migrated since PG17. */
-	if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1600)
-		return;
-
-	conn = connectToServer(&old_cluster, dbinfo->db_name);
-
 	/*
 	 * Fetch the logical replication slot information. The check whether the
 	 * slot is considered caught up is done by an upgrade function. This
@@ -673,18 +653,23 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
-							"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
-							"FROM pg_catalog.pg_replication_slots "
-							"WHERE slot_type = 'logical' AND "
-							"database = current_database() AND "
-							"temporary IS FALSE;",
-							user_opts.live_check ? "FALSE" :
-							"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
-							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
-							"END)");
-
-	num_slots = PQntuples(res);
+	return psprintf("SELECT slot_name, plugin, two_phase, failover, "
+					"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
+					"FROM pg_catalog.pg_replication_slots "
+					"WHERE slot_type = 'logical' AND "
+					"database = current_database() AND "
+					"temporary IS FALSE;",
+					user_opts.live_check ? "FALSE" :
+					"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
+					"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
+					"END)");
+}
+
+static void
+get_old_cluster_logical_slot_infos_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	LogicalSlotInfo *slotinfos = NULL;
+	int			num_slots = PQntuples(res);
 
 	if (num_slots)
 	{
@@ -717,14 +702,10 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 		}
 	}
 
-	PQclear(res);
-	PQfinish(conn);
-
 	dbinfo->slot_arr.slots = slotinfos;
 	dbinfo->slot_arr.nslots = num_slots;
 }
 
-
 /*
  * count_old_cluster_logical_slots()
  *
@@ -754,24 +735,18 @@ count_old_cluster_logical_slots(void)
  * This is because before that the logical slots are not upgraded, so we will
  * not be able to upgrade the logical replication clusters completely.
  */
-static void
-get_db_subscription_count(DbInfo *dbinfo)
+static char *
+get_db_subscription_count_query(DbInfo *dbinfo, void *arg)
 {
-	PGconn	   *conn;
-	PGresult   *res;
-
-	/* Subscriptions can be migrated since PG17. */
-	if (GET_MAJOR_VERSION(old_cluster.major_version) < 1700)
-		return;
+	return psprintf("SELECT count(*) "
+					"FROM pg_catalog.pg_subscription WHERE subdbid = %u",
+					dbinfo->db_oid);
+}
 
-	conn = connectToServer(&old_cluster, dbinfo->db_name);
-	res = executeQueryOrDie(conn, "SELECT count(*) "
-							"FROM pg_catalog.pg_subscription WHERE subdbid = %u",
-							dbinfo->db_oid);
+static void
+get_db_subscription_count_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
 	dbinfo->nsubs = atoi(PQgetvalue(res, 0, 0));
-
-	PQclear(res);
-	PQfinish(conn);
 }
 
 /*
-- 
2.39.3 (Apple Git-146)

v4-0005-use-new-pg_upgrade-async-API-to-parallelize-getti.patchtext/plain; charset=us-asciiDownload
From dbe59edcbe0d1b248456f67edc06cacfa5b06c69 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:24:35 -0500
Subject: [PATCH v4 05/12] use new pg_upgrade async API to parallelize getting
 loadable libraries

---
 src/bin/pg_upgrade/function.c | 63 ++++++++++++++++++++---------------
 1 file changed, 37 insertions(+), 26 deletions(-)

diff --git a/src/bin/pg_upgrade/function.c b/src/bin/pg_upgrade/function.c
index 7e3abed098..c11fce0696 100644
--- a/src/bin/pg_upgrade/function.c
+++ b/src/bin/pg_upgrade/function.c
@@ -42,6 +42,32 @@ library_name_compare(const void *p1, const void *p2)
 					  ((const LibraryInfo *) p2)->dbnum);
 }
 
+struct loadable_libraries_state
+{
+	PGresult  **ress;
+	int			totaltups;
+};
+
+static char *
+get_loadable_libraries_query(DbInfo *dbinfo, void *arg)
+{
+	return psprintf("SELECT DISTINCT probin "
+					"FROM pg_catalog.pg_proc "
+					"WHERE prolang = %u AND "
+					"probin IS NOT NULL AND "
+					"oid >= %u;",
+					ClanguageId,
+					FirstNormalObjectId);
+}
+
+static void
+get_loadable_libraries_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct loadable_libraries_state *state = (struct loadable_libraries_state *) arg;
+
+	state->ress[dbinfo - old_cluster.dbarr.dbs] = res;
+	state->totaltups += PQntuples(res);
+}
 
 /*
  * get_loadable_libraries()
@@ -54,47 +80,32 @@ library_name_compare(const void *p1, const void *p2)
 void
 get_loadable_libraries(void)
 {
-	PGresult  **ress;
 	int			totaltups;
 	int			dbnum;
 	int			n_libinfos;
+	AsyncTask  *task = async_task_create();
+	struct loadable_libraries_state state;
 
-	ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
-	totaltups = 0;
+	state.ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
+	state.totaltups = 0;
 
-	/* Fetch all library names, removing duplicates within each DB */
-	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
+	async_task_add_step(task, get_loadable_libraries_query,
+						get_loadable_libraries_result, false, &state);
 
-		/*
-		 * Fetch all libraries containing non-built-in C functions in this DB.
-		 */
-		ress[dbnum] = executeQueryOrDie(conn,
-										"SELECT DISTINCT probin "
-										"FROM pg_catalog.pg_proc "
-										"WHERE prolang = %u AND "
-										"probin IS NOT NULL AND "
-										"oid >= %u;",
-										ClanguageId,
-										FirstNormalObjectId);
-		totaltups += PQntuples(ress[dbnum]);
-
-		PQfinish(conn);
-	}
+	async_task_run(task, &old_cluster);
+	async_task_free(task);
 
 	/*
 	 * Allocate memory for required libraries and logical replication output
 	 * plugins.
 	 */
-	n_libinfos = totaltups + count_old_cluster_logical_slots();
+	n_libinfos = state.totaltups + count_old_cluster_logical_slots();
 	os_info.libraries = (LibraryInfo *) pg_malloc(sizeof(LibraryInfo) * n_libinfos);
 	totaltups = 0;
 
 	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
 	{
-		PGresult   *res = ress[dbnum];
+		PGresult   *res = state.ress[dbnum];
 		int			ntups;
 		int			rowno;
 		LogicalSlotInfoArr *slot_arr = &old_cluster.dbarr.dbs[dbnum].slot_arr;
@@ -129,7 +140,7 @@ get_loadable_libraries(void)
 		}
 	}
 
-	pg_free(ress);
+	pg_free(state.ress);
 
 	os_info.num_libraries = totaltups;
 }
-- 
2.39.3 (Apple Git-146)

v4-0006-use-new-pg_upgrade-async-API-to-parallelize-repor.patchtext/plain; charset=us-asciiDownload
From e7a87eec5317057ad8558a2a450a8b8fa5cc007d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:31:57 -0500
Subject: [PATCH v4 06/12] use new pg_upgrade async API to parallelize
 reporting extension updates

---
 src/bin/pg_upgrade/version.c | 82 ++++++++++++++++++------------------
 1 file changed, 41 insertions(+), 41 deletions(-)

diff --git a/src/bin/pg_upgrade/version.c b/src/bin/pg_upgrade/version.c
index 2de6dffccd..12783bb2ba 100644
--- a/src/bin/pg_upgrade/version.c
+++ b/src/bin/pg_upgrade/version.c
@@ -139,6 +139,42 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 		check_ok();
 }
 
+static char *
+report_extension_updates_query(DbInfo *dbinfo, void *arg)
+{
+	return pg_strdup("SELECT name "
+					 "FROM pg_available_extensions "
+					 "WHERE installed_version != default_version");
+}
+
+static void
+report_extension_updates_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_name = PQfnumber(res, "name");
+	char	   *output_path = "update_extensions.sql";
+	FILE	  **script = (FILE **) arg;
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL && (*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			PQExpBufferData connectbuf;
+
+			initPQExpBuffer(&connectbuf);
+			appendPsqlMetaConnect(&connectbuf, dbinfo->db_name);
+			fputs(connectbuf.data, *script);
+			termPQExpBuffer(&connectbuf);
+			db_used = true;
+		}
+		fprintf(*script, "ALTER EXTENSION %s UPDATE;\n",
+				quote_identifier(PQgetvalue(res, rowno, i_name)));
+	}
+}
+
 /*
  * report_extension_updates()
  *	Report extensions that should be updated.
@@ -146,53 +182,17 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 void
 report_extension_updates(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char	   *output_path = "update_extensions.sql";
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for extension updates");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_name;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* find extensions needing updates */
-		res = executeQueryOrDie(conn,
-								"SELECT name "
-								"FROM pg_available_extensions "
-								"WHERE installed_version != default_version"
-			);
-
-		ntups = PQntuples(res);
-		i_name = PQfnumber(res, "name");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				PQExpBufferData connectbuf;
-
-				initPQExpBuffer(&connectbuf);
-				appendPsqlMetaConnect(&connectbuf, active_db->db_name);
-				fputs(connectbuf.data, script);
-				termPQExpBuffer(&connectbuf);
-				db_used = true;
-			}
-			fprintf(script, "ALTER EXTENSION %s UPDATE;\n",
-					quote_identifier(PQgetvalue(res, rowno, i_name)));
-		}
+	async_task_add_step(task, report_extension_updates_query,
+						report_extension_updates_result, true, &script);
 
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v4-0007-parallelize-data-type-checks-in-pg_upgrade.patchtext/plain; charset=us-asciiDownload
From c6122d5b94375ebda9ebe7c2f78fb73b44780b78 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Sat, 6 Jul 2024 21:06:31 -0500
Subject: [PATCH v4 07/12] parallelize data type checks in pg_upgrade

---
 src/bin/pg_upgrade/check.c | 319 +++++++++++++++++++------------------
 1 file changed, 160 insertions(+), 159 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 251f3d9017..800d944218 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -32,6 +32,10 @@ static void check_new_cluster_subscription_configuration(void);
 static void check_old_cluster_for_valid_slots(void);
 static void check_old_cluster_subscription_state(void);
 
+static bool *data_type_check_results;
+static bool data_type_check_failed;
+static PQExpBufferData data_type_check_report;
+
 /*
  * DataTypesUsageChecks - definitions of data type checks for the old cluster
  * in order to determine if an upgrade can be performed.  See the comment on
@@ -314,6 +318,129 @@ static DataTypesUsageChecks data_types_usage_checks[] =
 	}
 };
 
+static char *
+data_type_check_query(DbInfo *dbinfo, void *arg)
+{
+	int			i = (int) (intptr_t) arg;
+	DataTypesUsageChecks *check = &data_types_usage_checks[i];
+
+	return psprintf("WITH RECURSIVE oids AS ( "
+	/* start with the type(s) returned by base_query */
+					"	%s "
+					"	UNION ALL "
+					"	SELECT * FROM ( "
+	/* inner WITH because we can only reference the CTE once */
+					"		WITH x AS (SELECT oid FROM oids) "
+	/* domains on any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
+					"			UNION ALL "
+	/* arrays over any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
+					"			UNION ALL "
+	/* composite types containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
+					"			WHERE t.typtype = 'c' AND "
+					"				  t.oid = c.reltype AND "
+					"				  c.oid = a.attrelid AND "
+					"				  NOT a.attisdropped AND "
+					"				  a.atttypid = x.oid "
+					"			UNION ALL "
+	/* ranges containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
+					"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
+					"	) foo "
+					") "
+	/* now look for stored columns of any such type */
+					"SELECT n.nspname, c.relname, a.attname "
+					"FROM	pg_catalog.pg_class c, "
+					"		pg_catalog.pg_namespace n, "
+					"		pg_catalog.pg_attribute a "
+					"WHERE	c.oid = a.attrelid AND "
+					"		NOT a.attisdropped AND "
+					"		a.atttypid IN (SELECT oid FROM oids) AND "
+					"		c.relkind IN ("
+					CppAsString2(RELKIND_RELATION) ", "
+					CppAsString2(RELKIND_MATVIEW) ", "
+					CppAsString2(RELKIND_INDEX) ") AND "
+					"		c.relnamespace = n.oid AND "
+	/* exclude possible orphaned temp tables */
+					"		n.nspname !~ '^pg_temp_' AND "
+					"		n.nspname !~ '^pg_toast_temp_' AND "
+	/* exclude system catalogs, too */
+					"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
+					check->base_query);
+}
+
+static void
+data_type_check_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	int			i = (int) (intptr_t) arg;
+	DataTypesUsageChecks *check = &data_types_usage_checks[i];
+	int			ntups = PQntuples(res);
+
+	if (ntups)
+	{
+		char		output_path[MAXPGPATH];
+		int			i_nspname;
+		int			i_relname;
+		int			i_attname;
+		FILE	   *script = NULL;
+		bool		db_used = false;
+
+		snprintf(output_path, sizeof(output_path), "%s/%s",
+				 log_opts.basedir,
+				 check->report_filename);
+
+		/*
+		 * Make sure we have a buffer to save reports to now that we found a
+		 * first failing check.
+		 */
+		if (!data_type_check_failed)
+			initPQExpBuffer(&data_type_check_report);
+		data_type_check_failed = true;
+
+		/*
+		 * If this is the first time we see an error for the check in question
+		 * then print a status message of the failure.
+		 */
+		if (!data_type_check_results[i])
+		{
+			pg_log(PG_REPORT, "    failed check: %s", _(check->status));
+			appendPQExpBuffer(&data_type_check_report, "\n%s\n%s    %s\n",
+							  _(check->report_text),
+							  _("A list of the problem columns is in the file:"),
+							  output_path);
+		}
+		data_type_check_results[i] = true;
+
+		i_nspname = PQfnumber(res, "nspname");
+		i_relname = PQfnumber(res, "relname");
+		i_attname = PQfnumber(res, "attname");
+
+		for (int rowno = 0; rowno < ntups; rowno++)
+		{
+			if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
+				pg_fatal("could not open file \"%s\": %m", output_path);
+
+			if (!db_used)
+			{
+				fprintf(script, "In database: %s\n", dbinfo->db_name);
+				db_used = true;
+			}
+			fprintf(script, "  %s.%s.%s\n",
+					PQgetvalue(res, rowno, i_nspname),
+					PQgetvalue(res, rowno, i_relname),
+					PQgetvalue(res, rowno, i_attname));
+		}
+
+		if (script)
+		{
+			fclose(script);
+			script = NULL;
+		}
+	}
+}
+
 /*
  * check_for_data_types_usage()
  *	Detect whether there are any stored columns depending on given type(s)
@@ -337,10 +464,9 @@ static void
 check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
 {
 	bool		found = false;
-	bool	   *results;
-	PQExpBufferData report;
 	DataTypesUsageChecks *tmp = checks;
 	int			n_data_types_usage_checks = 0;
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for data type usage");
 
@@ -352,176 +478,51 @@ check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
 	}
 
 	/* Prepare an array to store the results of checks in */
-	results = pg_malloc0(sizeof(bool) * n_data_types_usage_checks);
+	data_type_check_results = pg_malloc0(sizeof(bool) * n_data_types_usage_checks);
+	data_type_check_failed = false;
 
-	/*
-	 * Connect to each database in the cluster and run all defined checks
-	 * against that database before trying the next one.
-	 */
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int i = 0; i < n_data_types_usage_checks; i++)
 	{
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
+		DataTypesUsageChecks *check = &checks[i];
 
-		for (int checknum = 0; checknum < n_data_types_usage_checks; checknum++)
+		if (check->threshold_version == MANUAL_CHECK)
 		{
-			PGresult   *res;
-			int			ntups;
-			int			i_nspname;
-			int			i_relname;
-			int			i_attname;
-			FILE	   *script = NULL;
-			bool		db_used = false;
-			char		output_path[MAXPGPATH];
-			DataTypesUsageChecks *cur_check = &checks[checknum];
-
-			if (cur_check->threshold_version == MANUAL_CHECK)
-			{
-				Assert(cur_check->version_hook);
-
-				/*
-				 * Make sure that the check applies to the current cluster
-				 * version and skip if not. If no check hook has been defined
-				 * we run the check for all versions.
-				 */
-				if (!cur_check->version_hook(cluster))
-					continue;
-			}
-			else if (cur_check->threshold_version != ALL_VERSIONS)
-			{
-				if (GET_MAJOR_VERSION(cluster->major_version) > cur_check->threshold_version)
-					continue;
-			}
-			else
-				Assert(cur_check->threshold_version == ALL_VERSIONS);
-
-			snprintf(output_path, sizeof(output_path), "%s/%s",
-					 log_opts.basedir,
-					 cur_check->report_filename);
-
-			/*
-			 * The type(s) of interest might be wrapped in a domain, array,
-			 * composite, or range, and these container types can be nested
-			 * (to varying extents depending on server version, but that's not
-			 * of concern here).  To handle all these cases we need a
-			 * recursive CTE.
-			 */
-			res = executeQueryOrDie(conn,
-									"WITH RECURSIVE oids AS ( "
-			/* start with the type(s) returned by base_query */
-									"	%s "
-									"	UNION ALL "
-									"	SELECT * FROM ( "
-			/* inner WITH because we can only reference the CTE once */
-									"		WITH x AS (SELECT oid FROM oids) "
-			/* domains on any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
-									"			UNION ALL "
-			/* arrays over any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
-									"			UNION ALL "
-			/* composite types containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
-									"			WHERE t.typtype = 'c' AND "
-									"				  t.oid = c.reltype AND "
-									"				  c.oid = a.attrelid AND "
-									"				  NOT a.attisdropped AND "
-									"				  a.atttypid = x.oid "
-									"			UNION ALL "
-			/* ranges containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
-									"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
-									"	) foo "
-									") "
-			/* now look for stored columns of any such type */
-									"SELECT n.nspname, c.relname, a.attname "
-									"FROM	pg_catalog.pg_class c, "
-									"		pg_catalog.pg_namespace n, "
-									"		pg_catalog.pg_attribute a "
-									"WHERE	c.oid = a.attrelid AND "
-									"		NOT a.attisdropped AND "
-									"		a.atttypid IN (SELECT oid FROM oids) AND "
-									"		c.relkind IN ("
-									CppAsString2(RELKIND_RELATION) ", "
-									CppAsString2(RELKIND_MATVIEW) ", "
-									CppAsString2(RELKIND_INDEX) ") AND "
-									"		c.relnamespace = n.oid AND "
-			/* exclude possible orphaned temp tables */
-									"		n.nspname !~ '^pg_temp_' AND "
-									"		n.nspname !~ '^pg_toast_temp_' AND "
-			/* exclude system catalogs, too */
-									"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
-									cur_check->base_query);
-
-			ntups = PQntuples(res);
+			Assert(check->version_hook);
 
 			/*
-			 * The datatype was found, so extract the data and log to the
-			 * requested filename. We need to open the file for appending
-			 * since the check might have already found the type in another
-			 * database earlier in the loop.
+			 * Make sure that the check applies to the current cluster version
+			 * and skip it if not.
 			 */
-			if (ntups)
-			{
-				/*
-				 * Make sure we have a buffer to save reports to now that we
-				 * found a first failing check.
-				 */
-				if (!found)
-					initPQExpBuffer(&report);
-				found = true;
-
-				/*
-				 * If this is the first time we see an error for the check in
-				 * question then print a status message of the failure.
-				 */
-				if (!results[checknum])
-				{
-					pg_log(PG_REPORT, "    failed check: %s", _(cur_check->status));
-					appendPQExpBuffer(&report, "\n%s\n%s    %s\n",
-									  _(cur_check->report_text),
-									  _("A list of the problem columns is in the file:"),
-									  output_path);
-				}
-				results[checknum] = true;
-
-				i_nspname = PQfnumber(res, "nspname");
-				i_relname = PQfnumber(res, "relname");
-				i_attname = PQfnumber(res, "attname");
-
-				for (int rowno = 0; rowno < ntups; rowno++)
-				{
-					if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
-						pg_fatal("could not open file \"%s\": %m", output_path);
-
-					if (!db_used)
-					{
-						fprintf(script, "In database: %s\n", active_db->db_name);
-						db_used = true;
-					}
-					fprintf(script, "  %s.%s.%s\n",
-							PQgetvalue(res, rowno, i_nspname),
-							PQgetvalue(res, rowno, i_relname),
-							PQgetvalue(res, rowno, i_attname));
-				}
-
-				if (script)
-				{
-					fclose(script);
-					script = NULL;
-				}
-			}
-
-			PQclear(res);
+			if (!check->version_hook(cluster))
+				continue;
 		}
+		else if (check->threshold_version != ALL_VERSIONS)
+		{
+			if (GET_MAJOR_VERSION(cluster->major_version) > check->threshold_version)
+				continue;
+		}
+		else
+			Assert(check->threshold_version == ALL_VERSIONS);
 
-		PQfinish(conn);
+		async_task_add_step(task, data_type_check_query,
+							data_type_check_process, true,
+							(void *) (intptr_t) i);
 	}
 
+	/*
+	 * Connect to each database in the cluster and run all defined checks
+	 * against that database before trying the next one.
+	 */
+	async_task_run(task, cluster);
+	async_task_free(task);
+
 	if (found)
-		pg_fatal("Data type checks failed: %s", report.data);
+	{
+		pg_fatal("Data type checks failed: %s", data_type_check_report.data);
+		termPQExpBuffer(&data_type_check_report);
+	}
 
-	pg_free(results);
+	pg_free(data_type_check_results);
 
 	check_ok();
 }
-- 
2.39.3 (Apple Git-146)

v4-0008-parallelize-isn-and-int8-passing-mismatch-check-i.patchtext/plain; charset=us-asciiDownload
From 329b10464dcd377cc56a8eeb317a9c8e01ec8620 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:00:20 -0500
Subject: [PATCH v4 08/12] parallelize isn and int8 passing mismatch check in
 pg_upgrade

---
 src/bin/pg_upgrade/check.c | 85 ++++++++++++++++++++------------------
 1 file changed, 44 insertions(+), 41 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 800d944218..beb3fc9433 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1193,6 +1193,44 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 	check_ok();
 }
 
+static char *
+isn_and_int8_passing_mismatch_query(DbInfo *dbinfo, void *arg)
+{
+	return pg_strdup("SELECT n.nspname, p.proname "
+					 "FROM   pg_catalog.pg_proc p, "
+					 "       pg_catalog.pg_namespace n "
+					 "WHERE  p.pronamespace = n.oid AND "
+					 "       p.probin = '$libdir/isn'");
+}
+
+static void
+isn_and_int8_passing_mismatch_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_proname = PQfnumber(res, "proname");
+	FILE	  **script = (FILE **) arg;
+	char		output_path[MAXPGPATH];
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "contrib_isn_and_int8_pass_by_value.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL && (*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_proname));
+	}
+}
 
 /*
  *	check_for_isn_and_int8_passing_mismatch()
@@ -1204,8 +1242,8 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 static void
 check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
+	AsyncTask  *task;
 	char		output_path[MAXPGPATH];
 
 	prep_status("Checking for contrib/isn with bigint-passing mismatch");
@@ -1222,46 +1260,11 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "contrib_isn_and_int8_pass_by_value.txt");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_proname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* Find any functions coming from contrib/isn */
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, p.proname "
-								"FROM	pg_catalog.pg_proc p, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	p.pronamespace = n.oid AND "
-								"		p.probin = '$libdir/isn'");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_proname = PQfnumber(res, "proname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_proname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	task = async_task_create();
+	async_task_add_step(task, isn_and_int8_passing_mismatch_query,
+						isn_and_int8_passing_mismatch_process, true, &script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v4-0009-parallelize-user-defined-postfix-ops-check-in-pg_.patchtext/plain; charset=us-asciiDownload
From 102a1146b421dd0218d61586d2b4b65e5dc4cd86 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:12:49 -0500
Subject: [PATCH v4 09/12] parallelize user defined postfix ops check in
 pg_upgrade

---
 src/bin/pg_upgrade/check.c | 134 +++++++++++++++++++------------------
 1 file changed, 69 insertions(+), 65 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index beb3fc9433..6b61db65c5 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1282,15 +1282,79 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 		check_ok();
 }
 
+static char *
+user_defined_postfix_ops_query(DbInfo *dbinfo, void *arg)
+{
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	return pg_strdup("SELECT o.oid AS oproid, "
+					 "       n.nspname AS oprnsp, "
+					 "       o.oprname, "
+					 "       tn.nspname AS typnsp, "
+					 "       t.typname "
+					 "FROM pg_catalog.pg_operator o, "
+					 "     pg_catalog.pg_namespace n, "
+					 "     pg_catalog.pg_type t, "
+					 "     pg_catalog.pg_namespace tn "
+					 "WHERE o.oprnamespace = n.oid AND "
+					 "      o.oprleft = t.oid AND "
+					 "      t.typnamespace = tn.oid AND "
+					 "      o.oprright = 0 AND "
+					 "      o.oid >= 16384");
+}
+
+static void
+user_defined_postfix_ops_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	FILE	  **script = (FILE **) arg;
+	char		output_path[MAXPGPATH];
+	int			ntups = PQntuples(res);
+	bool		db_used = false;
+	int			i_oproid = PQfnumber(res, "oproid");
+	int			i_oprnsp = PQfnumber(res, "oprnsp");
+	int			i_oprname = PQfnumber(res, "oprname");
+	int			i_typnsp = PQfnumber(res, "typnsp");
+	int			i_typname = PQfnumber(res, "typname");
+
+	if (!ntups)
+		return;
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "postfix_ops.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL &&
+			(*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
+				PQgetvalue(res, rowno, i_oproid),
+				PQgetvalue(res, rowno, i_oprnsp),
+				PQgetvalue(res, rowno, i_oprname),
+				PQgetvalue(res, rowno, i_typnsp),
+				PQgetvalue(res, rowno, i_typname));
+	}
+}
+
 /*
  * Verify that no user defined postfix operators exist.
  */
 static void
 check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for user-defined postfix operators");
 
@@ -1298,70 +1362,10 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "postfix_ops.txt");
 
-	/* Find any user defined postfix operators */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_oproid,
-					i_oprnsp,
-					i_oprname,
-					i_typnsp,
-					i_typname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT o.oid AS oproid, "
-								"       n.nspname AS oprnsp, "
-								"       o.oprname, "
-								"       tn.nspname AS typnsp, "
-								"       t.typname "
-								"FROM pg_catalog.pg_operator o, "
-								"     pg_catalog.pg_namespace n, "
-								"     pg_catalog.pg_type t, "
-								"     pg_catalog.pg_namespace tn "
-								"WHERE o.oprnamespace = n.oid AND "
-								"      o.oprleft = t.oid AND "
-								"      t.typnamespace = tn.oid AND "
-								"      o.oprright = 0 AND "
-								"      o.oid >= 16384");
-		ntups = PQntuples(res);
-		i_oproid = PQfnumber(res, "oproid");
-		i_oprnsp = PQfnumber(res, "oprnsp");
-		i_oprname = PQfnumber(res, "oprname");
-		i_typnsp = PQfnumber(res, "typnsp");
-		i_typname = PQfnumber(res, "typname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
-					PQgetvalue(res, rowno, i_oproid),
-					PQgetvalue(res, rowno, i_oprnsp),
-					PQgetvalue(res, rowno, i_oprname),
-					PQgetvalue(res, rowno, i_typnsp),
-					PQgetvalue(res, rowno, i_typname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_add_step(task, user_defined_postfix_ops_query,
+						user_defined_postfix_ops_process, true, &script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v4-0010-parallelize-incompatible-polymorphics-check-in-pg.patchtext/plain; charset=us-asciiDownload
From 679bf95bea485149173543b6a89b0bf98209b40a Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:30:19 -0500
Subject: [PATCH v4 10/12] parallelize incompatible polymorphics check in
 pg_upgrade

---
 src/bin/pg_upgrade/check.c       | 174 ++++++++++++++++---------------
 src/tools/pgindent/typedefs.list |   1 +
 2 files changed, 92 insertions(+), 83 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 6b61db65c5..6c11732e70 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1381,6 +1381,81 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 		check_ok();
 }
 
+typedef struct incompat_polymorphics_state
+{
+	FILE	   *script;
+	PQExpBufferData old_polymorphics;
+	char		output_path[MAXPGPATH];
+} incompat_polymorphics_state;
+
+static char *
+incompat_polymorphics_query(DbInfo *dbinfo, void *arg)
+{
+	incompat_polymorphics_state *state = (incompat_polymorphics_state *) arg;
+
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	/* Aggregate transition functions */
+	return psprintf("SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					"FROM pg_proc AS p "
+					"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					"JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
+					"WHERE p.oid >= 16384 "
+					"AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
+					"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Aggregate final functions */
+					"UNION ALL "
+					"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					"FROM pg_proc AS p "
+					"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					"JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
+					"WHERE p.oid >= 16384 "
+					"AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
+					"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Operators */
+					"UNION ALL "
+					"SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
+					"FROM pg_operator AS op "
+					"WHERE op.oid >= 16384 "
+					"AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
+					"AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
+					state->old_polymorphics.data,
+					state->old_polymorphics.data,
+					state->old_polymorphics.data);
+}
+
+static void
+incompat_polymorphics_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	incompat_polymorphics_state *state = (incompat_polymorphics_state *) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_objkind = PQfnumber(res, "objkind");
+	int			i_objname = PQfnumber(res, "objname");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (state->script == NULL &&
+			(state->script = fopen_priv(state->output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state->output_path);
+		if (!db_used)
+		{
+			fprintf(state->script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+
+		fprintf(state->script, "  %s: %s\n",
+				PQgetvalue(res, rowno, i_objkind),
+				PQgetvalue(res, rowno, i_objname));
+	}
+}
+
 /*
  *	check_for_incompatible_polymorphics()
  *
@@ -1390,111 +1465,44 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 static void
 check_for_incompatible_polymorphics(ClusterInfo *cluster)
 {
-	PGresult   *res;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
-	PQExpBufferData old_polymorphics;
+	AsyncTask  *task = async_task_create();
+	incompat_polymorphics_state state;
 
 	prep_status("Checking for incompatible polymorphic functions");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	state.script = NULL;
+	snprintf(state.output_path, sizeof(state.output_path), "%s/%s",
 			 log_opts.basedir,
 			 "incompatible_polymorphics.txt");
 
 	/* The set of problematic functions varies a bit in different versions */
-	initPQExpBuffer(&old_polymorphics);
+	initPQExpBuffer(&state.old_polymorphics);
 
-	appendPQExpBufferStr(&old_polymorphics,
+	appendPQExpBufferStr(&state.old_polymorphics,
 						 "'array_append(anyarray,anyelement)'"
 						 ", 'array_cat(anyarray,anyarray)'"
 						 ", 'array_prepend(anyelement,anyarray)'");
 
 	if (GET_MAJOR_VERSION(cluster->major_version) >= 903)
-		appendPQExpBufferStr(&old_polymorphics,
+		appendPQExpBufferStr(&state.old_polymorphics,
 							 ", 'array_remove(anyarray,anyelement)'"
 							 ", 'array_replace(anyarray,anyelement,anyelement)'");
 
 	if (GET_MAJOR_VERSION(cluster->major_version) >= 905)
-		appendPQExpBufferStr(&old_polymorphics,
+		appendPQExpBufferStr(&state.old_polymorphics,
 							 ", 'array_position(anyarray,anyelement)'"
 							 ", 'array_position(anyarray,anyelement,integer)'"
 							 ", 'array_positions(anyarray,anyelement)'"
 							 ", 'width_bucket(anyelement,anyarray)'");
 
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		bool		db_used = false;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-		int			ntups;
-		int			i_objkind,
-					i_objname;
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-		/* Aggregate transition functions */
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Aggregate final functions */
-								"UNION ALL "
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Operators */
-								"UNION ALL "
-								"SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
-								"FROM pg_operator AS op "
-								"WHERE op.oid >= 16384 "
-								"AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
-								old_polymorphics.data,
-								old_polymorphics.data,
-								old_polymorphics.data);
-
-		ntups = PQntuples(res);
-
-		i_objkind = PQfnumber(res, "objkind");
-		i_objname = PQfnumber(res, "objname");
-
-		for (int rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-
-			fprintf(script, "  %s: %s\n",
-					PQgetvalue(res, rowno, i_objkind),
-					PQgetvalue(res, rowno, i_objname));
-		}
-
-		PQclear(res);
-		PQfinish(conn);
-	}
+	async_task_add_step(task, incompat_polymorphics_query,
+						incompat_polymorphics_process, true, &state);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
-	if (script)
+	if (state.script)
 	{
-		fclose(script);
+		fclose(state.script);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains user-defined objects that refer to internal\n"
 				 "polymorphic functions with arguments of type \"anyarray\" or \"anyelement\".\n"
@@ -1502,12 +1510,12 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 				 "afterwards, changing them to refer to the new corresponding functions with\n"
 				 "arguments of type \"anycompatiblearray\" and \"anycompatible\".\n"
 				 "A list of the problematic objects is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", state.output_path);
 	}
 	else
 		check_ok();
 
-	termPQExpBuffer(&old_polymorphics);
+	termPQExpBuffer(&state.old_polymorphics);
 }
 
 /*
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index bbd8827c2d..8555c2c6bd 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3535,6 +3535,7 @@ hstoreUpgrade_t
 hyperLogLogState
 ifState
 import_error_callback_arg
+incompat_polymorphics_state
 indexed_tlist
 inet
 inetKEY
-- 
2.39.3 (Apple Git-146)

v4-0011-parallelize-tables-with-oids-check-in-pg_upgrade.patchtext/plain; charset=us-asciiDownload
From 0c51fe8e3ddfaa59c24dd5351a55d025a8fb2681 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:42:22 -0500
Subject: [PATCH v4 11/12] parallelize tables with oids check in pg_upgrade

---
 src/bin/pg_upgrade/check.c | 90 ++++++++++++++++++++------------------
 1 file changed, 48 insertions(+), 42 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 6c11732e70..639a62d426 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1518,15 +1518,58 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 	termPQExpBuffer(&state.old_polymorphics);
 }
 
+static char *
+with_oids_query(DbInfo *dbinfo, void *arg)
+{
+	return pg_strdup("SELECT n.nspname, c.relname "
+					 "FROM   pg_catalog.pg_class c, "
+					 "       pg_catalog.pg_namespace n "
+					 "WHERE  c.relnamespace = n.oid AND "
+					 "       c.relhasoids AND"
+					 "       n.nspname NOT IN ('pg_catalog')");
+}
+
+static void
+with_oids_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	char		output_path[MAXPGPATH];
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+	FILE	  **script = (FILE **) arg;
+
+	if (!ntups)
+		return;
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "tables_with_oids.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL && (*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_relname));
+	}
+}
+
 /*
  * Verify that no tables are declared WITH OIDS.
  */
 static void
 check_for_tables_with_oids(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for tables WITH OIDS");
 
@@ -1534,47 +1577,10 @@ check_for_tables_with_oids(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "tables_with_oids.txt");
 
-	/* Find any tables declared WITH OIDS */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_relname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, c.relname "
-								"FROM	pg_catalog.pg_class c, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	c.relnamespace = n.oid AND "
-								"		c.relhasoids AND"
-								"       n.nspname NOT IN ('pg_catalog')");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_relname = PQfnumber(res, "relname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_relname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_add_step(task, with_oids_query,
+						with_oids_process, true, &script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v4-0001-introduce-framework-for-parallelizing-pg_upgrade-.patchtext/plain; charset=us-asciiDownload
From a4110826fd703e100da7ca8419e4d0609b4346eb Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 11:02:44 -0500
Subject: [PATCH v4 01/12] introduce framework for parallelizing pg_upgrade
 tasks

---
 src/bin/pg_upgrade/Makefile      |   1 +
 src/bin/pg_upgrade/async.c       | 323 +++++++++++++++++++++++++++++++
 src/bin/pg_upgrade/meson.build   |   1 +
 src/bin/pg_upgrade/pg_upgrade.h  |  16 ++
 src/tools/pgindent/typedefs.list |   4 +
 5 files changed, 345 insertions(+)
 create mode 100644 src/bin/pg_upgrade/async.c

diff --git a/src/bin/pg_upgrade/Makefile b/src/bin/pg_upgrade/Makefile
index bde91e2beb..3bc4f5d740 100644
--- a/src/bin/pg_upgrade/Makefile
+++ b/src/bin/pg_upgrade/Makefile
@@ -12,6 +12,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	$(WIN32RES) \
+	async.o \
 	check.o \
 	controldata.o \
 	dump.o \
diff --git a/src/bin/pg_upgrade/async.c b/src/bin/pg_upgrade/async.c
new file mode 100644
index 0000000000..7df1e7712d
--- /dev/null
+++ b/src/bin/pg_upgrade/async.c
@@ -0,0 +1,323 @@
+/*
+ * async.c
+ *
+ * parallelization via libpq's async APIs
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ * src/bin/pg_upgrade/async.c
+ */
+
+#include "postgres_fe.h"
+
+#include "common/connect.h"
+#include "fe_utils/string_utils.h"
+#include "pg_upgrade.h"
+
+static int	dbs_complete;
+static int	dbs_processing;
+
+typedef struct AsyncTaskCallbacks
+{
+	AsyncTaskGetQueryCB query_cb;
+	AsyncTaskProcessCB process_cb;
+	bool		free_result;
+	void	   *arg;
+} AsyncTaskCallbacks;
+
+typedef struct AsyncTask
+{
+	AsyncTaskCallbacks *cbs;
+	int			num_cb_sets;
+} AsyncTask;
+
+typedef enum
+{
+	FREE,
+	CONNECTING,
+	SETTING_SEARCH_PATH,
+	RUNNING_QUERY,
+} AsyncSlotState;
+
+typedef struct
+{
+	AsyncSlotState state;
+	int			db;
+	int			query;
+	PGconn	   *conn;
+} AsyncSlot;
+
+AsyncTask *
+async_task_create(void)
+{
+	return pg_malloc0(sizeof(AsyncTask));
+}
+
+void
+async_task_free(AsyncTask *task)
+{
+	if (task->cbs)
+		pg_free(task->cbs);
+
+	pg_free(task);
+}
+
+void
+async_task_add_step(AsyncTask *task,
+					AsyncTaskGetQueryCB query_cb,
+					AsyncTaskProcessCB process_cb, bool free_result,
+					void *arg)
+{
+	AsyncTaskCallbacks *new_cbs;
+
+	task->cbs = pg_realloc(task->cbs,
+						   ++task->num_cb_sets * sizeof(AsyncTaskCallbacks));
+
+	new_cbs = &task->cbs[task->num_cb_sets - 1];
+	new_cbs->query_cb = query_cb;
+	new_cbs->process_cb = process_cb;
+	new_cbs->free_result = free_result;
+	new_cbs->arg = arg;
+}
+
+static void
+conn_failure(PGconn *conn)
+{
+	pg_log(PG_REPORT, "%s", PQerrorMessage(conn));
+	printf(_("Failure, exiting\n"));
+	exit(1);
+}
+
+static void
+start_conn(const ClusterInfo *cluster, AsyncSlot *slot)
+{
+	PQExpBufferData conn_opts;
+
+	/* Build connection string with proper quoting */
+	initPQExpBuffer(&conn_opts);
+	appendPQExpBufferStr(&conn_opts, "dbname=");
+	appendConnStrVal(&conn_opts, cluster->dbarr.dbs[slot->db].db_name);
+	appendPQExpBufferStr(&conn_opts, " user=");
+	appendConnStrVal(&conn_opts, os_info.user);
+	appendPQExpBuffer(&conn_opts, " port=%d", cluster->port);
+	if (cluster->sockdir)
+	{
+		appendPQExpBufferStr(&conn_opts, " host=");
+		appendConnStrVal(&conn_opts, cluster->sockdir);
+	}
+
+	slot->conn = PQconnectStart(conn_opts.data);
+	termPQExpBuffer(&conn_opts);
+
+	if (!slot->conn)
+		conn_failure(slot->conn);
+}
+
+static void
+dispatch_query(const ClusterInfo *cluster, AsyncSlot *slot,
+			   const AsyncTask *task)
+{
+	AsyncTaskCallbacks *cbs = &task->cbs[slot->query];
+	AsyncTaskGetQueryCB get_query = cbs->query_cb;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db];
+	char	   *query = (*get_query) (dbinfo, cbs->arg);
+
+	if (!PQsendQuery(slot->conn, query))
+		conn_failure(slot->conn);
+
+	pg_free(query);
+}
+
+static PGresult *
+get_last_result(PGconn *conn)
+{
+	PGresult   *tmp;
+	PGresult   *res = NULL;
+
+	while ((tmp = PQgetResult(conn)) != NULL)
+	{
+		PQclear(res);
+		res = tmp;
+		if (PQstatus(conn) == CONNECTION_BAD)
+			conn_failure(conn);
+	}
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK &&
+		PQresultStatus(res) != PGRES_TUPLES_OK)
+		conn_failure(conn);
+
+	return res;
+}
+
+static void
+process_query_result(const ClusterInfo *cluster, AsyncSlot *slot,
+					 const AsyncTask *task)
+{
+	AsyncTaskCallbacks *cbs = &task->cbs[slot->query];
+	AsyncTaskProcessCB process_cb = cbs->process_cb;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db];
+	PGresult   *res = get_last_result(slot->conn);
+
+	(*process_cb) (dbinfo, res, cbs->arg);
+
+	if (cbs->free_result)
+		PQclear(res);
+}
+
+static void
+process_slot(const ClusterInfo *cluster, AsyncSlot *slot, const AsyncTask *task)
+{
+	switch (slot->state)
+	{
+		case FREE:
+			if (dbs_processing >= cluster->dbarr.ndbs)
+				return;
+			slot->db = dbs_processing++;
+			slot->state = CONNECTING;
+			start_conn(cluster, slot);
+			return;
+
+		case CONNECTING:
+			if (PQconnectPoll(slot->conn) == PGRES_POLLING_FAILED)
+				conn_failure(slot->conn);
+			if (PQconnectPoll(slot->conn) != PGRES_POLLING_OK)
+				return;
+			slot->state = SETTING_SEARCH_PATH;
+			if (!PQsendQuery(slot->conn, ALWAYS_SECURE_SEARCH_PATH_SQL))
+				conn_failure(slot->conn);
+			return;
+
+		case SETTING_SEARCH_PATH:
+			if (!PQconsumeInput(slot->conn))
+				conn_failure(slot->conn);
+			if (PQisBusy(slot->conn))
+				return;
+			PQclear(get_last_result(slot->conn));
+			slot->state = RUNNING_QUERY;
+			dispatch_query(cluster, slot, task);
+			return;
+
+		case RUNNING_QUERY:
+			if (!PQconsumeInput(slot->conn))
+				conn_failure(slot->conn);
+			if (PQisBusy(slot->conn))
+				return;
+			process_query_result(cluster, slot, task);
+			if (++slot->query >= task->num_cb_sets)
+			{
+				dbs_complete++;
+				PQfinish(slot->conn);
+				memset(slot, 0, sizeof(AsyncSlot));
+				return;
+			}
+			dispatch_query(cluster, slot, task);
+			return;
+	}
+}
+
+/*
+ * Wait on the slots to either finish connecting or to receive query results if
+ * possible.  This avoids a tight loop in async_task_run().
+ */
+static void
+wait_on_slots(AsyncSlot *slots, int numslots)
+{
+	fd_set		input_mask;
+	fd_set		output_mask;
+	fd_set		except_mask;
+	int			maxFd = 0;
+
+	FD_ZERO(&input_mask);
+	FD_ZERO(&output_mask);
+	FD_ZERO(&except_mask);
+
+	for (int i = 0; i < numslots; i++)
+	{
+		int			sock;
+		bool		read = false;
+
+		switch (slots[i].state)
+		{
+			case FREE:
+
+				/*
+				 * If we see a free slot, return right away so that it can be
+				 * reused immediately for the next database.  This might cause
+				 * us to spin more than necessary as we finish processing the
+				 * last few databases, but that shouldn't cause too much harm.
+				 */
+				return;
+
+			case CONNECTING:
+
+				/*
+				 * If we are waiting for the connection to establish, choose
+				 * whether to wait for reading or for writing on the socket as
+				 * appropriate.  If neither apply, just return immediately so
+				 * that we can handle the slot.
+				 */
+				{
+					PostgresPollingStatusType status;
+
+					status = PQconnectPoll(slots[i].conn);
+					if (status == PGRES_POLLING_READING)
+						read = true;
+					else if (status != PGRES_POLLING_WRITING)
+						return;
+				}
+				break;
+
+			case SETTING_SEARCH_PATH:
+			case RUNNING_QUERY:
+
+				/*
+				 * If we've sent a query, we must wait for the socket to be
+				 * read-ready.  Note that process_slot() handles calling
+				 * PQconsumeInput() as required.
+				 */
+				read = true;
+				break;
+		}
+
+		/*
+		 * If there's some problem retrieving the socket, just pretend this
+		 * slot doesn't exist.  We don't expect this to happen regularly in
+		 * practice, so it seems unlikely to cause too much harm.
+		 */
+		sock = PQsocket(slots[i].conn);
+		if (sock < 0)
+			continue;
+
+		/*
+		 * Add the socket to the set.
+		 */
+		FD_SET(sock, read ? &input_mask : &output_mask);
+		FD_SET(sock, &except_mask);
+		maxFd = Max(maxFd, sock);
+	}
+
+	/*
+	 * If we found socket(s) to wait on, wait.
+	 */
+	if (maxFd != 0)
+		(void) select(maxFd + 1, &input_mask, &output_mask, &except_mask, NULL);
+}
+
+void
+async_task_run(const AsyncTask *task, const ClusterInfo *cluster)
+{
+	int			jobs = Max(1, user_opts.jobs);
+	AsyncSlot  *slots = pg_malloc0(sizeof(AsyncSlot) * jobs);
+
+	dbs_complete = 0;
+	dbs_processing = 0;
+
+	while (dbs_complete < cluster->dbarr.ndbs)
+	{
+		for (int i = 0; i < jobs; i++)
+			process_slot(cluster, &slots[i], task);
+
+		wait_on_slots(slots, jobs);
+	}
+
+	pg_free(slots);
+}
diff --git a/src/bin/pg_upgrade/meson.build b/src/bin/pg_upgrade/meson.build
index 9825fa3305..9eb48e176c 100644
--- a/src/bin/pg_upgrade/meson.build
+++ b/src/bin/pg_upgrade/meson.build
@@ -1,6 +1,7 @@
 # Copyright (c) 2022-2024, PostgreSQL Global Development Group
 
 pg_upgrade_sources = files(
+  'async.c',
   'check.c',
   'controldata.c',
   'dump.c',
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 8afe240bdf..1ebad3bd74 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -494,3 +494,19 @@ void		parallel_transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr
 										  char *old_pgdata, char *new_pgdata,
 										  char *old_tablespace);
 bool		reap_child(bool wait_for_child);
+
+/* async.c */
+
+typedef char *(*AsyncTaskGetQueryCB) (DbInfo *dbinfo, void *arg);
+typedef void (*AsyncTaskProcessCB) (DbInfo *dbinfo, PGresult *res, void *arg);
+
+/* struct definition is private to async.c */
+typedef struct AsyncTask AsyncTask;
+
+AsyncTask  *async_task_create(void);
+void		async_task_add_step(AsyncTask *task,
+								AsyncTaskGetQueryCB query_cb,
+								AsyncTaskProcessCB process_cb, bool free_result,
+								void *arg);
+void		async_task_run(const AsyncTask *task, const ClusterInfo *cluster);
+void		async_task_free(AsyncTask *task);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 635e6d6e21..bbd8827c2d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -153,6 +153,10 @@ ArrayMetaState
 ArraySubWorkspace
 ArrayToken
 ArrayType
+AsyncSlot
+AsyncSlotState
+AsyncTask
+AsyncTaskCallbacks
 AsyncQueueControl
 AsyncQueueEntry
 AsyncRequest
-- 
2.39.3 (Apple Git-146)

v4-0012-parallelize-user-defined-encoding-conversions-che.patchtext/plain; charset=us-asciiDownload
From 9c64682938929eb7da0d901213471c14b7fb0227 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:52:13 -0500
Subject: [PATCH v4 12/12] parallelize user defined encoding conversions check
 in pg_upgrade

---
 src/bin/pg_upgrade/check.c | 107 ++++++++++++++++++++-----------------
 1 file changed, 57 insertions(+), 50 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 639a62d426..b20651c851 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1654,15 +1654,66 @@ check_for_pg_role_prefix(ClusterInfo *cluster)
 		check_ok();
 }
 
+static char *
+user_defined_encoding_conversions_query(DbInfo *dbinfo, void *arg)
+{
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	return pstrdup("SELECT c.oid as conoid, c.conname, n.nspname "
+				   "FROM pg_catalog.pg_conversion c, "
+				   "     pg_catalog.pg_namespace n "
+				   "WHERE c.connamespace = n.oid AND "
+				   "      c.oid >= 16384");
+}
+
+static void
+user_defined_encoding_conversions_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	FILE	  **script = (FILE **) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	char		output_path[MAXPGPATH];
+	int			i_conoid = PQfnumber(res, "conoid");
+	int			i_conname = PQfnumber(res, "conname");
+	int			i_nspname = PQfnumber(res, "nspname");
+
+	if (!ntups)
+		return;
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "encoding_conversions.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL &&
+			(*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  (oid=%s) %s.%s\n",
+				PQgetvalue(res, rowno, i_conoid),
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_conname));
+	}
+}
+
 /*
  * Verify that no user-defined encoding conversions exist.
  */
 static void
 check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for user-defined encoding conversions");
 
@@ -1670,55 +1721,11 @@ check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "encoding_conversions.txt");
 
-	/* Find any user defined encoding conversions */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_conoid,
-					i_conname,
-					i_nspname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT c.oid as conoid, c.conname, n.nspname "
-								"FROM pg_catalog.pg_conversion c, "
-								"     pg_catalog.pg_namespace n "
-								"WHERE c.connamespace = n.oid AND "
-								"      c.oid >= 16384");
-		ntups = PQntuples(res);
-		i_conoid = PQfnumber(res, "conoid");
-		i_conname = PQfnumber(res, "conname");
-		i_nspname = PQfnumber(res, "nspname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s\n",
-					PQgetvalue(res, rowno, i_conoid),
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_conname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_add_step(task, user_defined_encoding_conversions_query,
+						user_defined_encoding_conversions_process, true,
+						&script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

#9Daniel Gustafsson
daniel@yesql.se
In reply to: Nathan Bossart (#8)
Re: optimizing pg_upgrade's once-in-each-database steps

On 9 Jul 2024, at 05:33, Nathan Bossart <nathandbossart@gmail.com> wrote:

The code is still very rough and nowhere near committable, but this at
least gets the patch set into the editing phase.

First reaction after a read-through is that this seems really cool, can't wait
to see how much v18 pg_upgrade will be over v17. I will do more testing and
review once back from vacation, below are some comments from reading which is
all I had time for at this point:

+static void
+conn_failure(PGconn *conn)
+{
+   pg_log(PG_REPORT, "%s", PQerrorMessage(conn));
+   printf(_("Failure, exiting\n"));
+   exit(1);
+}

Any particular reason this isn't using pg_fatal()?

+static void
+dispatch_query(const ClusterInfo *cluster, AsyncSlot *slot,
    ....
+   pg_free(query);
+}

A minor point, perhaps fueled by me not having played around much with this
patchset. It seems a bit odd that dispatch_query is responsible for freeing
the query from the get_query callback. I would have expected the output from
AsyncTaskGetQueryCB to be stored in AsyncTask and released by async_task_free.

+static void
+sub_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
     ....
+       fprintf(state->script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
+               PQgetvalue(res, i, 0),
+               dbinfo->db_name,
+               PQgetvalue(res, i, 1),

With the query being in a separate place in the code from the processing it
takes a bit of jumping around to resolve the columns in PQgetvalue calls like
this. Using PQfnumber() calls and descriptive names would make this easier. I
know this case is copying old behavior, but the function splits make it less
useful than before.

+ char *query = pg_malloc(QUERY_ALLOC);

Should we convert this to a PQExpBuffer?

--
Daniel Gustafsson

#10Nathan Bossart
nathandbossart@gmail.com
In reply to: Daniel Gustafsson (#9)
Re: optimizing pg_upgrade's once-in-each-database steps

On Wed, Jul 17, 2024 at 11:16:59PM +0200, Daniel Gustafsson wrote:

First reaction after a read-through is that this seems really cool, can't wait
to see how much v18 pg_upgrade will be over v17. I will do more testing and
review once back from vacation, below are some comments from reading which is
all I had time for at this point:

Thanks for taking a look!

+static void
+conn_failure(PGconn *conn)
+{
+   pg_log(PG_REPORT, "%s", PQerrorMessage(conn));
+   printf(_("Failure, exiting\n"));
+   exit(1);
+}

Any particular reason this isn't using pg_fatal()?

IIRC this was to match the error in connectToServer(). We could probably
move to pg_fatal().

+static void
+dispatch_query(const ClusterInfo *cluster, AsyncSlot *slot,
....
+   pg_free(query);
+}

A minor point, perhaps fueled by me not having played around much with this
patchset. It seems a bit odd that dispatch_query is responsible for freeing
the query from the get_query callback. I would have expected the output from
AsyncTaskGetQueryCB to be stored in AsyncTask and released by async_task_free.

I don't see any problem with doing it the way you suggest.

Tangentially related, I waffled a bit on whether to require the query
callbacks to put the result in allocated memory. Some queries are the same
no matter what, and some require customization at runtime. As you can see,
I ended up just requiring allocated memory. That makes the code a tad
simpler, and I doubt the extra work is noticeable.

+static void
+sub_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
....
+       fprintf(state->script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
+               PQgetvalue(res, i, 0),
+               dbinfo->db_name,
+               PQgetvalue(res, i, 1),

With the query being in a separate place in the code from the processing it
takes a bit of jumping around to resolve the columns in PQgetvalue calls like
this. Using PQfnumber() calls and descriptive names would make this easier. I
know this case is copying old behavior, but the function splits make it less
useful than before.

Good point.

+ char *query = pg_malloc(QUERY_ALLOC);

Should we convert this to a PQExpBuffer?

Seems like a good idea. I think I was trying to change as few lines as
possible for my proof-of-concept. :)

--
nathan

#11Daniel Gustafsson
daniel@yesql.se
In reply to: Nathan Bossart (#10)
Re: optimizing pg_upgrade's once-in-each-database steps

On 17 Jul 2024, at 23:32, Nathan Bossart <nathandbossart@gmail.com> wrote:
On Wed, Jul 17, 2024 at 11:16:59PM +0200, Daniel Gustafsson wrote:

+static void
+dispatch_query(const ClusterInfo *cluster, AsyncSlot *slot,
....
+   pg_free(query);
+}

A minor point, perhaps fueled by me not having played around much with this
patchset. It seems a bit odd that dispatch_query is responsible for freeing
the query from the get_query callback. I would have expected the output from
AsyncTaskGetQueryCB to be stored in AsyncTask and released by async_task_free.

I don't see any problem with doing it the way you suggest.

Tangentially related, I waffled a bit on whether to require the query
callbacks to put the result in allocated memory. Some queries are the same
no matter what, and some require customization at runtime. As you can see,
I ended up just requiring allocated memory. That makes the code a tad
simpler, and I doubt the extra work is noticeable.

I absolutely agree with this.

+ char *query = pg_malloc(QUERY_ALLOC);

Should we convert this to a PQExpBuffer?

Seems like a good idea. I think I was trying to change as few lines as
possible for my proof-of-concept. :)

Yeah, that's a good approach, I just noticed it while reading the hunks. We
can do that separately from this patchset.

In order to trim down the size of the patchset I think going ahead with 0003
independently of this makes sense, it has value by itself.

--
Daniel Gustafsson

#12Nathan Bossart
nathandbossart@gmail.com
In reply to: Daniel Gustafsson (#11)
Re: optimizing pg_upgrade's once-in-each-database steps

On Thu, Jul 18, 2024 at 09:57:23AM +0200, Daniel Gustafsson wrote:

On 17 Jul 2024, at 23:32, Nathan Bossart <nathandbossart@gmail.com> wrote:
On Wed, Jul 17, 2024 at 11:16:59PM +0200, Daniel Gustafsson wrote:

+static void
+dispatch_query(const ClusterInfo *cluster, AsyncSlot *slot,
....
+   pg_free(query);
+}

A minor point, perhaps fueled by me not having played around much with this
patchset. It seems a bit odd that dispatch_query is responsible for freeing
the query from the get_query callback. I would have expected the output from
AsyncTaskGetQueryCB to be stored in AsyncTask and released by async_task_free.

I don't see any problem with doing it the way you suggest.

Actually, I do see a problem. If we do it this way, we'll have to store a
string per database somewhere, which seems unnecessary.

However, while looking into this, I noticed that only one get_query
callback (get_db_subscription_count()) actually customizes the generated
query using information in the provided DbInfo. AFAICT we can do this
particular step without running a query in each database, as I mentioned
elsewhere [0]/messages/by-id/ZprQJv_TxccN3tkr@nathan. That should speed things up a bit and allow us to simplify
the AsyncTask code.

With that, if we are willing to assume that a given get_query callback will
generate the same string for all databases (and I think we should), we can
run the callback once and save the string in the step for dispatch_query()
to use. This would look more like what you suggested in the quoted text.

[0]: /messages/by-id/ZprQJv_TxccN3tkr@nathan

--
nathan

#13Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#12)
13 attachment(s)
Re: optimizing pg_upgrade's once-in-each-database steps

On Fri, Jul 19, 2024 at 04:21:37PM -0500, Nathan Bossart wrote:

However, while looking into this, I noticed that only one get_query
callback (get_db_subscription_count()) actually customizes the generated
query using information in the provided DbInfo. AFAICT we can do this
particular step without running a query in each database, as I mentioned
elsewhere [0]. That should speed things up a bit and allow us to simplify
the AsyncTask code.

With that, if we are willing to assume that a given get_query callback will
generate the same string for all databases (and I think we should), we can
run the callback once and save the string in the step for dispatch_query()
to use. This would look more like what you suggested in the quoted text.

Here is a new patch set. I've included the latest revision of the patch to
fix get_db_subscription_count() from the other thread [0]https://commitfest.postgresql.org/49/5135/ as 0001 since I
expect that to be committed soon. I've also moved the patch that moves the
"live_check" variable to "user_opts" to 0002 since I plan on committing
that sooner than later, too. Otherwise, I've tried to address all feedback
provided thus far.

[0]: https://commitfest.postgresql.org/49/5135/

--
nathan

Attachments:

v5-0001-pg_upgrade-retrieve-subscription-count-more-effic.patchtext/plain; charset=us-asciiDownload
From 75b0c739f3710034ff8910176460c3bfbeb1716c Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Sat, 20 Jul 2024 21:01:29 -0500
Subject: [PATCH v5 01/13] pg_upgrade: retrieve subscription count more
 efficiently

---
 src/bin/pg_upgrade/check.c      |  9 +++----
 src/bin/pg_upgrade/info.c       | 43 +++++++--------------------------
 src/bin/pg_upgrade/pg_upgrade.h |  3 +--
 3 files changed, 13 insertions(+), 42 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 27924159d6..39d14b7b92 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1797,17 +1797,14 @@ check_new_cluster_subscription_configuration(void)
 {
 	PGresult   *res;
 	PGconn	   *conn;
-	int			nsubs_on_old;
 	int			max_replication_slots;
 
 	/* Subscriptions and their dependencies can be migrated since PG17. */
 	if (GET_MAJOR_VERSION(old_cluster.major_version) < 1700)
 		return;
 
-	nsubs_on_old = count_old_cluster_subscriptions();
-
 	/* Quick return if there are no subscriptions to be migrated. */
-	if (nsubs_on_old == 0)
+	if (old_cluster.nsubs == 0)
 		return;
 
 	prep_status("Checking for new cluster configuration for subscriptions");
@@ -1821,10 +1818,10 @@ check_new_cluster_subscription_configuration(void)
 		pg_fatal("could not determine parameter settings on new cluster");
 
 	max_replication_slots = atoi(PQgetvalue(res, 0, 0));
-	if (nsubs_on_old > max_replication_slots)
+	if (old_cluster.nsubs > max_replication_slots)
 		pg_fatal("\"max_replication_slots\" (%d) must be greater than or equal to the number of "
 				 "subscriptions (%d) on the old cluster",
-				 max_replication_slots, nsubs_on_old);
+				 max_replication_slots, old_cluster.nsubs);
 
 	PQclear(res);
 	PQfinish(conn);
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 95c22a7200..e43be79aa5 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -28,7 +28,7 @@ static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
 static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check);
-static void get_db_subscription_count(DbInfo *dbinfo);
+static void get_subscription_count(ClusterInfo *cluster);
 
 
 /*
@@ -293,17 +293,13 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check)
 
 		get_rel_infos(cluster, pDbInfo);
 
-		/*
-		 * Retrieve the logical replication slots infos and the subscriptions
-		 * count for the old cluster.
-		 */
 		if (cluster == &old_cluster)
-		{
 			get_old_cluster_logical_slot_infos(pDbInfo, live_check);
-			get_db_subscription_count(pDbInfo);
-		}
 	}
 
+	if (cluster == &old_cluster)
+		get_subscription_count(cluster);
+
 	if (cluster == &old_cluster)
 		pg_log(PG_VERBOSE, "\nsource databases:");
 	else
@@ -750,14 +746,14 @@ count_old_cluster_logical_slots(void)
 /*
  * get_db_subscription_count()
  *
- * Gets the number of subscriptions in the database referred to by "dbinfo".
+ * Gets the number of subscriptions in the cluster.
  *
  * Note: This function will not do anything if the old cluster is pre-PG17.
  * This is because before that the logical slots are not upgraded, so we will
  * not be able to upgrade the logical replication clusters completely.
  */
 static void
-get_db_subscription_count(DbInfo *dbinfo)
+get_subscription_count(ClusterInfo *cluster)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -766,36 +762,15 @@ get_db_subscription_count(DbInfo *dbinfo)
 	if (GET_MAJOR_VERSION(old_cluster.major_version) < 1700)
 		return;
 
-	conn = connectToServer(&old_cluster, dbinfo->db_name);
+	conn = connectToServer(&old_cluster, "template1");
 	res = executeQueryOrDie(conn, "SELECT count(*) "
-							"FROM pg_catalog.pg_subscription WHERE subdbid = %u",
-							dbinfo->db_oid);
-	dbinfo->nsubs = atoi(PQgetvalue(res, 0, 0));
+							"FROM pg_catalog.pg_subscription");
+	cluster->nsubs = atoi(PQgetvalue(res, 0, 0));
 
 	PQclear(res);
 	PQfinish(conn);
 }
 
-/*
- * count_old_cluster_subscriptions()
- *
- * Returns the number of subscriptions for all databases.
- *
- * Note: this function always returns 0 if the old_cluster is PG16 and prior
- * because we gather subscriptions only for cluster versions greater than or
- * equal to PG17. See get_db_subscription_count().
- */
-int
-count_old_cluster_subscriptions(void)
-{
-	int			nsubs = 0;
-
-	for (int dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-		nsubs += old_cluster.dbarr.dbs[dbnum].nsubs;
-
-	return nsubs;
-}
-
 static void
 free_db_and_rel_infos(DbInfoArr *db_arr)
 {
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 8afe240bdf..e6dbbe6a93 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -197,7 +197,6 @@ typedef struct
 											 * path */
 	RelInfoArr	rel_arr;		/* array of all user relinfos */
 	LogicalSlotInfoArr slot_arr;	/* array of all LogicalSlotInfo */
-	int			nsubs;			/* number of subscriptions */
 } DbInfo;
 
 /*
@@ -296,6 +295,7 @@ typedef struct
 	char		major_version_str[64];	/* string PG_VERSION of cluster */
 	uint32		bin_version;	/* version returned from pg_ctl */
 	const char *tablespace_suffix;	/* directory specification */
+	int			nsubs;			/* number of subscriptions */
 } ClusterInfo;
 
 
@@ -430,7 +430,6 @@ FileNameMap *gen_db_file_maps(DbInfo *old_db,
 							  const char *new_pgdata);
 void		get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check);
 int			count_old_cluster_logical_slots(void);
-int			count_old_cluster_subscriptions(void);
 
 /* option.c */
 
-- 
2.39.3 (Apple Git-146)

v5-0002-move-live_check-variable-to-user_opts.patchtext/plain; charset=us-asciiDownload
From d727dad6968d58464bf8044901badf024ade795e Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 21 May 2024 16:35:19 -0500
Subject: [PATCH v5 02/13] move live_check variable to user_opts

---
 src/bin/pg_upgrade/check.c       | 32 ++++++++++++++++----------------
 src/bin/pg_upgrade/controldata.c |  5 +++--
 src/bin/pg_upgrade/info.c        | 12 +++++-------
 src/bin/pg_upgrade/option.c      |  4 ++--
 src/bin/pg_upgrade/pg_upgrade.c  | 21 ++++++++++-----------
 src/bin/pg_upgrade/pg_upgrade.h  | 13 +++++++------
 6 files changed, 43 insertions(+), 44 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 39d14b7b92..d8888f5053 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -29,7 +29,7 @@ static void check_for_new_tablespace_dir(void);
 static void check_for_user_defined_encoding_conversions(ClusterInfo *cluster);
 static void check_new_cluster_logical_replication_slots(void);
 static void check_new_cluster_subscription_configuration(void);
-static void check_old_cluster_for_valid_slots(bool live_check);
+static void check_old_cluster_for_valid_slots(void);
 static void check_old_cluster_subscription_state(void);
 
 /*
@@ -555,9 +555,9 @@ fix_path_separator(char *path)
 }
 
 void
-output_check_banner(bool live_check)
+output_check_banner(void)
 {
-	if (user_opts.check && live_check)
+	if (user_opts.live_check)
 	{
 		pg_log(PG_REPORT,
 			   "Performing Consistency Checks on Old Live Server\n"
@@ -573,18 +573,18 @@ output_check_banner(bool live_check)
 
 
 void
-check_and_dump_old_cluster(bool live_check)
+check_and_dump_old_cluster(void)
 {
 	/* -- OLD -- */
 
-	if (!live_check)
+	if (!user_opts.live_check)
 		start_postmaster(&old_cluster, true);
 
 	/*
 	 * Extract a list of databases, tables, and logical replication slots from
 	 * the old cluster.
 	 */
-	get_db_rel_and_slot_infos(&old_cluster, live_check);
+	get_db_rel_and_slot_infos(&old_cluster);
 
 	init_tablespaces();
 
@@ -605,7 +605,7 @@ check_and_dump_old_cluster(bool live_check)
 		 * Logical replication slots can be migrated since PG17. See comments
 		 * atop get_old_cluster_logical_slot_infos().
 		 */
-		check_old_cluster_for_valid_slots(live_check);
+		check_old_cluster_for_valid_slots();
 
 		/*
 		 * Subscriptions and their dependencies can be migrated since PG17.
@@ -652,7 +652,7 @@ check_and_dump_old_cluster(bool live_check)
 	 */
 	if (GET_MAJOR_VERSION(old_cluster.major_version) <= 906)
 	{
-		if (user_opts.check)
+		if (user_opts.live_check)
 			old_9_6_invalidate_hash_indexes(&old_cluster, true);
 	}
 
@@ -667,7 +667,7 @@ check_and_dump_old_cluster(bool live_check)
 	if (!user_opts.check)
 		generate_old_dump();
 
-	if (!live_check)
+	if (!user_opts.live_check)
 		stop_postmaster(false);
 }
 
@@ -675,7 +675,7 @@ check_and_dump_old_cluster(bool live_check)
 void
 check_new_cluster(void)
 {
-	get_db_rel_and_slot_infos(&new_cluster, false);
+	get_db_rel_and_slot_infos(&new_cluster);
 
 	check_new_cluster_is_empty();
 
@@ -826,14 +826,14 @@ check_cluster_versions(void)
 
 
 void
-check_cluster_compatibility(bool live_check)
+check_cluster_compatibility(void)
 {
 	/* get/check pg_control data of servers */
-	get_control_data(&old_cluster, live_check);
-	get_control_data(&new_cluster, false);
+	get_control_data(&old_cluster);
+	get_control_data(&new_cluster);
 	check_control_data(&old_cluster.controldata, &new_cluster.controldata);
 
-	if (live_check && old_cluster.port == new_cluster.port)
+	if (user_opts.live_check && old_cluster.port == new_cluster.port)
 		pg_fatal("When checking a live server, "
 				 "the old and new port numbers must be different.");
 }
@@ -1836,7 +1836,7 @@ check_new_cluster_subscription_configuration(void)
  * before shutdown.
  */
 static void
-check_old_cluster_for_valid_slots(bool live_check)
+check_old_cluster_for_valid_slots(void)
 {
 	char		output_path[MAXPGPATH];
 	FILE	   *script = NULL;
@@ -1875,7 +1875,7 @@ check_old_cluster_for_valid_slots(bool live_check)
 			 * Note: This can be satisfied only when the old cluster has been
 			 * shut down, so we skip this for live checks.
 			 */
-			if (!live_check && !slot->caught_up)
+			if (!user_opts.live_check && !slot->caught_up)
 			{
 				if (script == NULL &&
 					(script = fopen_priv(output_path, "w")) == NULL)
diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c
index 1f0ccea3ed..cf665b9dee 100644
--- a/src/bin/pg_upgrade/controldata.c
+++ b/src/bin/pg_upgrade/controldata.c
@@ -33,7 +33,7 @@
  * return valid xid data for a running server.
  */
 void
-get_control_data(ClusterInfo *cluster, bool live_check)
+get_control_data(ClusterInfo *cluster)
 {
 	char		cmd[MAXPGPATH];
 	char		bufin[MAX_STRING];
@@ -76,6 +76,7 @@ get_control_data(ClusterInfo *cluster, bool live_check)
 	uint32		segno = 0;
 	char	   *resetwal_bin;
 	int			rc;
+	bool		live_check = (cluster == &old_cluster && user_opts.live_check);
 
 	/*
 	 * Because we test the pg_resetwal output as strings, it has to be in
@@ -118,7 +119,7 @@ get_control_data(ClusterInfo *cluster, bool live_check)
 	/*
 	 * Check for clean shutdown
 	 */
-	if (!live_check || cluster == &new_cluster)
+	if (!live_check)
 	{
 		/* only pg_controldata outputs the cluster state */
 		snprintf(cmd, sizeof(cmd), "\"%s/pg_controldata\" \"%s\"",
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index e43be79aa5..fc94de9ea4 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -27,7 +27,7 @@ static void free_rel_infos(RelInfoArr *rel_arr);
 static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
-static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check);
+static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo);
 static void get_subscription_count(ClusterInfo *cluster);
 
 
@@ -273,11 +273,9 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
  *
  * higher level routine to generate dbinfos for the database running
  * on the given "port". Assumes that server is already running.
- *
- * live_check would be used only when the target is the old cluster.
  */
 void
-get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check)
+get_db_rel_and_slot_infos(ClusterInfo *cluster)
 {
 	int			dbnum;
 
@@ -294,7 +292,7 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check)
 		get_rel_infos(cluster, pDbInfo);
 
 		if (cluster == &old_cluster)
-			get_old_cluster_logical_slot_infos(pDbInfo, live_check);
+			get_old_cluster_logical_slot_infos(pDbInfo);
 	}
 
 	if (cluster == &old_cluster)
@@ -641,7 +639,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
  * are included.
  */
 static void
-get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
+get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -677,7 +675,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 							"WHERE slot_type = 'logical' AND "
 							"database = current_database() AND "
 							"temporary IS FALSE;",
-							live_check ? "FALSE" :
+							user_opts.live_check ? "FALSE" :
 							"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
 							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
 							"END)");
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index 548ea4e623..6f41d63eed 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -470,10 +470,10 @@ adjust_data_dir(ClusterInfo *cluster)
  * directory.
  */
 void
-get_sock_dir(ClusterInfo *cluster, bool live_check)
+get_sock_dir(ClusterInfo *cluster)
 {
 #if !defined(WIN32)
-	if (!live_check)
+	if (!user_opts.live_check || cluster == &new_cluster)
 		cluster->sockdir = user_opts.socketdir;
 	else
 	{
diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c
index 03eb738fd7..99f3d4543e 100644
--- a/src/bin/pg_upgrade/pg_upgrade.c
+++ b/src/bin/pg_upgrade/pg_upgrade.c
@@ -65,7 +65,7 @@ static void create_new_objects(void);
 static void copy_xact_xlog_xid(void);
 static void set_frozenxids(bool minmxid_only);
 static void make_outputdirs(char *pgdata);
-static void setup(char *argv0, bool *live_check);
+static void setup(char *argv0);
 static void create_logical_replication_slots(void);
 
 ClusterInfo old_cluster,
@@ -88,7 +88,6 @@ int
 main(int argc, char **argv)
 {
 	char	   *deletion_script_file_name = NULL;
-	bool		live_check = false;
 
 	/*
 	 * pg_upgrade doesn't currently use common/logging.c, but initialize it
@@ -123,18 +122,18 @@ main(int argc, char **argv)
 	 */
 	make_outputdirs(new_cluster.pgdata);
 
-	setup(argv[0], &live_check);
+	setup(argv[0]);
 
-	output_check_banner(live_check);
+	output_check_banner();
 
 	check_cluster_versions();
 
-	get_sock_dir(&old_cluster, live_check);
-	get_sock_dir(&new_cluster, false);
+	get_sock_dir(&old_cluster);
+	get_sock_dir(&new_cluster);
 
-	check_cluster_compatibility(live_check);
+	check_cluster_compatibility();
 
-	check_and_dump_old_cluster(live_check);
+	check_and_dump_old_cluster();
 
 
 	/* -- NEW -- */
@@ -331,7 +330,7 @@ make_outputdirs(char *pgdata)
 
 
 static void
-setup(char *argv0, bool *live_check)
+setup(char *argv0)
 {
 	/*
 	 * make sure the user has a clean environment, otherwise, we may confuse
@@ -378,7 +377,7 @@ setup(char *argv0, bool *live_check)
 				pg_fatal("There seems to be a postmaster servicing the old cluster.\n"
 						 "Please shutdown that postmaster and try again.");
 			else
-				*live_check = true;
+				user_opts.live_check = true;
 		}
 	}
 
@@ -660,7 +659,7 @@ create_new_objects(void)
 		set_frozenxids(true);
 
 	/* update new_cluster info now that we have objects in the databases */
-	get_db_rel_and_slot_infos(&new_cluster, false);
+	get_db_rel_and_slot_infos(&new_cluster);
 }
 
 /*
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index e6dbbe6a93..08b2ad26d7 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -322,6 +322,7 @@ typedef struct
 typedef struct
 {
 	bool		check;			/* check clusters only, don't change any data */
+	bool		live_check;		/* check clusters only, old server is running */
 	bool		do_sync;		/* flush changes to disk */
 	transferMode transfer_mode; /* copy files or link them? */
 	int			jobs;			/* number of processes/threads to use */
@@ -366,20 +367,20 @@ extern OSInfo os_info;
 
 /* check.c */
 
-void		output_check_banner(bool live_check);
-void		check_and_dump_old_cluster(bool live_check);
+void		output_check_banner(void);
+void		check_and_dump_old_cluster(void);
 void		check_new_cluster(void);
 void		report_clusters_compatible(void);
 void		issue_warnings_and_set_wal_level(void);
 void		output_completion_banner(char *deletion_script_file_name);
 void		check_cluster_versions(void);
-void		check_cluster_compatibility(bool live_check);
+void		check_cluster_compatibility(void);
 void		create_script_for_old_cluster_deletion(char **deletion_script_file_name);
 
 
 /* controldata.c */
 
-void		get_control_data(ClusterInfo *cluster, bool live_check);
+void		get_control_data(ClusterInfo *cluster);
 void		check_control_data(ControlData *oldctrl, ControlData *newctrl);
 void		disable_old_cluster(void);
 
@@ -428,14 +429,14 @@ void		check_loadable_libraries(void);
 FileNameMap *gen_db_file_maps(DbInfo *old_db,
 							  DbInfo *new_db, int *nmaps, const char *old_pgdata,
 							  const char *new_pgdata);
-void		get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check);
+void		get_db_rel_and_slot_infos(ClusterInfo *cluster);
 int			count_old_cluster_logical_slots(void);
 
 /* option.c */
 
 void		parseCommandLine(int argc, char *argv[]);
 void		adjust_data_dir(ClusterInfo *cluster);
-void		get_sock_dir(ClusterInfo *cluster, bool live_check);
+void		get_sock_dir(ClusterInfo *cluster);
 
 /* relfilenumber.c */
 
-- 
2.39.3 (Apple Git-146)

v5-0003-introduce-framework-for-parallelizing-pg_upgrade-.patchtext/plain; charset=us-asciiDownload
From c955d1d1d769f8864a43cf98dc9dbacf5e6c7f9c Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 11:02:44 -0500
Subject: [PATCH v5 03/13] introduce framework for parallelizing pg_upgrade
 tasks

---
 src/bin/pg_upgrade/Makefile      |   1 +
 src/bin/pg_upgrade/async.c       | 327 +++++++++++++++++++++++++++++++
 src/bin/pg_upgrade/meson.build   |   1 +
 src/bin/pg_upgrade/pg_upgrade.h  |  16 ++
 src/tools/pgindent/typedefs.list |   4 +
 5 files changed, 349 insertions(+)
 create mode 100644 src/bin/pg_upgrade/async.c

diff --git a/src/bin/pg_upgrade/Makefile b/src/bin/pg_upgrade/Makefile
index bde91e2beb..3bc4f5d740 100644
--- a/src/bin/pg_upgrade/Makefile
+++ b/src/bin/pg_upgrade/Makefile
@@ -12,6 +12,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	$(WIN32RES) \
+	async.o \
 	check.o \
 	controldata.o \
 	dump.o \
diff --git a/src/bin/pg_upgrade/async.c b/src/bin/pg_upgrade/async.c
new file mode 100644
index 0000000000..742e9df37c
--- /dev/null
+++ b/src/bin/pg_upgrade/async.c
@@ -0,0 +1,327 @@
+/*
+ * async.c
+ *
+ * parallelization via libpq's async APIs
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ * src/bin/pg_upgrade/async.c
+ */
+
+#include "postgres_fe.h"
+
+#include "common/connect.h"
+#include "fe_utils/string_utils.h"
+#include "pg_upgrade.h"
+
+static int	dbs_complete;
+static int	dbs_processing;
+
+typedef struct AsyncTaskCallbacks
+{
+	AsyncTaskGetQueryCB query_cb;
+	AsyncTaskProcessCB process_cb;
+	char	   *query;
+	bool		free_result;
+	void	   *arg;
+} AsyncTaskCallbacks;
+
+typedef struct AsyncTask
+{
+	AsyncTaskCallbacks *cbs;
+	int			num_cb_sets;
+} AsyncTask;
+
+typedef enum
+{
+	FREE,
+	CONNECTING,
+	SETTING_SEARCH_PATH,
+	RUNNING_QUERY,
+} AsyncSlotState;
+
+typedef struct
+{
+	AsyncSlotState state;
+	int			db;
+	int			query;
+	PGconn	   *conn;
+} AsyncSlot;
+
+AsyncTask *
+async_task_create(void)
+{
+	return pg_malloc0(sizeof(AsyncTask));
+}
+
+void
+async_task_free(AsyncTask *task)
+{
+	for (int i = 0; i < task->num_cb_sets; i++)
+	{
+		if (task->cbs[i].query)
+			pg_free(task->cbs[i].query);
+	}
+
+	if (task->cbs)
+		pg_free(task->cbs);
+
+	pg_free(task);
+}
+
+void
+async_task_add_step(AsyncTask *task,
+					AsyncTaskGetQueryCB query_cb,
+					AsyncTaskProcessCB process_cb, bool free_result,
+					void *arg)
+{
+	AsyncTaskCallbacks *new_cbs;
+
+	task->cbs = pg_realloc(task->cbs,
+						   ++task->num_cb_sets * sizeof(AsyncTaskCallbacks));
+
+	new_cbs = &task->cbs[task->num_cb_sets - 1];
+	new_cbs->query_cb = query_cb;
+	new_cbs->process_cb = process_cb;
+	new_cbs->query = NULL;
+	new_cbs->free_result = free_result;
+	new_cbs->arg = arg;
+}
+
+static void
+conn_failure(PGconn *conn)
+{
+	pg_fatal("connection failure: %s", PQerrorMessage(conn));
+}
+
+static void
+start_conn(const ClusterInfo *cluster, AsyncSlot *slot)
+{
+	PQExpBufferData conn_opts;
+
+	/* Build connection string with proper quoting */
+	initPQExpBuffer(&conn_opts);
+	appendPQExpBufferStr(&conn_opts, "dbname=");
+	appendConnStrVal(&conn_opts, cluster->dbarr.dbs[slot->db].db_name);
+	appendPQExpBufferStr(&conn_opts, " user=");
+	appendConnStrVal(&conn_opts, os_info.user);
+	appendPQExpBuffer(&conn_opts, " port=%d", cluster->port);
+	if (cluster->sockdir)
+	{
+		appendPQExpBufferStr(&conn_opts, " host=");
+		appendConnStrVal(&conn_opts, cluster->sockdir);
+	}
+
+	slot->conn = PQconnectStart(conn_opts.data);
+	termPQExpBuffer(&conn_opts);
+
+	if (!slot->conn)
+		conn_failure(slot->conn);
+}
+
+static void
+dispatch_query(const ClusterInfo *cluster, AsyncSlot *slot,
+			   const AsyncTask *task)
+{
+	AsyncTaskCallbacks *cbs = &task->cbs[slot->query];
+
+	if (!cbs->query)
+		cbs->query = cbs->query_cb(cbs->arg);
+
+	if (!PQsendQuery(slot->conn, cbs->query))
+		conn_failure(slot->conn);
+}
+
+static PGresult *
+get_last_result(PGconn *conn)
+{
+	PGresult   *tmp;
+	PGresult   *res = NULL;
+
+	while ((tmp = PQgetResult(conn)) != NULL)
+	{
+		PQclear(res);
+		res = tmp;
+		if (PQstatus(conn) == CONNECTION_BAD)
+			conn_failure(conn);
+	}
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK &&
+		PQresultStatus(res) != PGRES_TUPLES_OK)
+		conn_failure(conn);
+
+	return res;
+}
+
+static void
+process_query_result(const ClusterInfo *cluster, AsyncSlot *slot,
+					 const AsyncTask *task)
+{
+	AsyncTaskCallbacks *cbs = &task->cbs[slot->query];
+	AsyncTaskProcessCB process_cb = cbs->process_cb;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db];
+	PGresult   *res = get_last_result(slot->conn);
+
+	(*process_cb) (dbinfo, res, cbs->arg);
+
+	if (cbs->free_result)
+		PQclear(res);
+}
+
+static void
+process_slot(const ClusterInfo *cluster, AsyncSlot *slot, const AsyncTask *task)
+{
+	switch (slot->state)
+	{
+		case FREE:
+			if (dbs_processing >= cluster->dbarr.ndbs)
+				return;
+			slot->db = dbs_processing++;
+			slot->state = CONNECTING;
+			start_conn(cluster, slot);
+			return;
+
+		case CONNECTING:
+			if (PQconnectPoll(slot->conn) == PGRES_POLLING_FAILED)
+				conn_failure(slot->conn);
+			if (PQconnectPoll(slot->conn) != PGRES_POLLING_OK)
+				return;
+			slot->state = SETTING_SEARCH_PATH;
+			if (!PQsendQuery(slot->conn, ALWAYS_SECURE_SEARCH_PATH_SQL))
+				conn_failure(slot->conn);
+			return;
+
+		case SETTING_SEARCH_PATH:
+			if (!PQconsumeInput(slot->conn))
+				conn_failure(slot->conn);
+			if (PQisBusy(slot->conn))
+				return;
+			PQclear(get_last_result(slot->conn));
+			slot->state = RUNNING_QUERY;
+			dispatch_query(cluster, slot, task);
+			return;
+
+		case RUNNING_QUERY:
+			if (!PQconsumeInput(slot->conn))
+				conn_failure(slot->conn);
+			if (PQisBusy(slot->conn))
+				return;
+			process_query_result(cluster, slot, task);
+			if (++slot->query >= task->num_cb_sets)
+			{
+				dbs_complete++;
+				PQfinish(slot->conn);
+				memset(slot, 0, sizeof(AsyncSlot));
+				return;
+			}
+			dispatch_query(cluster, slot, task);
+			return;
+	}
+}
+
+/*
+ * Wait on the slots to either finish connecting or to receive query results if
+ * possible.  This avoids a tight loop in async_task_run().
+ */
+static void
+wait_on_slots(AsyncSlot *slots, int numslots)
+{
+	fd_set		input_mask;
+	fd_set		output_mask;
+	fd_set		except_mask;
+	int			maxFd = 0;
+
+	FD_ZERO(&input_mask);
+	FD_ZERO(&output_mask);
+	FD_ZERO(&except_mask);
+
+	for (int i = 0; i < numslots; i++)
+	{
+		int			sock;
+		bool		read = false;
+
+		switch (slots[i].state)
+		{
+			case FREE:
+
+				/*
+				 * If we see a free slot, return right away so that it can be
+				 * reused immediately for the next database.  This might cause
+				 * us to spin more than necessary as we finish processing the
+				 * last few databases, but that shouldn't cause too much harm.
+				 */
+				return;
+
+			case CONNECTING:
+
+				/*
+				 * If we are waiting for the connection to establish, choose
+				 * whether to wait for reading or for writing on the socket as
+				 * appropriate.  If neither apply, just return immediately so
+				 * that we can handle the slot.
+				 */
+				{
+					PostgresPollingStatusType status;
+
+					status = PQconnectPoll(slots[i].conn);
+					if (status == PGRES_POLLING_READING)
+						read = true;
+					else if (status != PGRES_POLLING_WRITING)
+						return;
+				}
+				break;
+
+			case SETTING_SEARCH_PATH:
+			case RUNNING_QUERY:
+
+				/*
+				 * If we've sent a query, we must wait for the socket to be
+				 * read-ready.  Note that process_slot() handles calling
+				 * PQconsumeInput() as required.
+				 */
+				read = true;
+				break;
+		}
+
+		/*
+		 * If there's some problem retrieving the socket, just pretend this
+		 * slot doesn't exist.  We don't expect this to happen regularly in
+		 * practice, so it seems unlikely to cause too much harm.
+		 */
+		sock = PQsocket(slots[i].conn);
+		if (sock < 0)
+			continue;
+
+		/*
+		 * Add the socket to the set.
+		 */
+		FD_SET(sock, read ? &input_mask : &output_mask);
+		FD_SET(sock, &except_mask);
+		maxFd = Max(maxFd, sock);
+	}
+
+	/*
+	 * If we found socket(s) to wait on, wait.
+	 */
+	if (maxFd != 0)
+		(void) select(maxFd + 1, &input_mask, &output_mask, &except_mask, NULL);
+}
+
+void
+async_task_run(const AsyncTask *task, const ClusterInfo *cluster)
+{
+	int			jobs = Max(1, user_opts.jobs);
+	AsyncSlot  *slots = pg_malloc0(sizeof(AsyncSlot) * jobs);
+
+	dbs_complete = 0;
+	dbs_processing = 0;
+
+	while (dbs_complete < cluster->dbarr.ndbs)
+	{
+		for (int i = 0; i < jobs; i++)
+			process_slot(cluster, &slots[i], task);
+
+		wait_on_slots(slots, jobs);
+	}
+
+	pg_free(slots);
+}
diff --git a/src/bin/pg_upgrade/meson.build b/src/bin/pg_upgrade/meson.build
index 9825fa3305..9eb48e176c 100644
--- a/src/bin/pg_upgrade/meson.build
+++ b/src/bin/pg_upgrade/meson.build
@@ -1,6 +1,7 @@
 # Copyright (c) 2022-2024, PostgreSQL Global Development Group
 
 pg_upgrade_sources = files(
+  'async.c',
   'check.c',
   'controldata.c',
   'dump.c',
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 08b2ad26d7..dea418ec3f 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -494,3 +494,19 @@ void		parallel_transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr
 										  char *old_pgdata, char *new_pgdata,
 										  char *old_tablespace);
 bool		reap_child(bool wait_for_child);
+
+/* async.c */
+
+typedef char *(*AsyncTaskGetQueryCB) (void *arg);
+typedef void (*AsyncTaskProcessCB) (DbInfo *dbinfo, PGresult *res, void *arg);
+
+/* struct definition is private to async.c */
+typedef struct AsyncTask AsyncTask;
+
+AsyncTask  *async_task_create(void);
+void		async_task_add_step(AsyncTask *task,
+								AsyncTaskGetQueryCB query_cb,
+								AsyncTaskProcessCB process_cb, bool free_result,
+								void *arg);
+void		async_task_run(const AsyncTask *task, const ClusterInfo *cluster);
+void		async_task_free(AsyncTask *task);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index b4d7f9217c..a2330f4346 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -153,6 +153,10 @@ ArrayMetaState
 ArraySubWorkspace
 ArrayToken
 ArrayType
+AsyncSlot
+AsyncSlotState
+AsyncTask
+AsyncTaskCallbacks
 AsyncQueueControl
 AsyncQueueEntry
 AsyncRequest
-- 
2.39.3 (Apple Git-146)

v5-0004-use-new-pg_upgrade-async-API-for-subscription-sta.patchtext/plain; charset=us-asciiDownload
From 917a33304f1eb20065351075f487d2deb4f8e976 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 17:21:19 -0500
Subject: [PATCH v5 04/13] use new pg_upgrade async API for subscription state
 checks

---
 src/bin/pg_upgrade/check.c | 204 ++++++++++++++++++++-----------------
 1 file changed, 110 insertions(+), 94 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index d8888f5053..41a23e7efe 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1903,6 +1903,79 @@ check_old_cluster_for_valid_slots(void)
 	check_ok();
 }
 
+/* private state for subscription state checks */
+struct substate_info
+{
+	FILE	   *script;
+	char		output_path[MAXPGPATH];
+};
+
+/*
+ * We don't allow upgrade if there is a risk of dangling slot or origin
+ * corresponding to initial sync after upgrade.
+ *
+ * A slot/origin not created yet refers to the 'i' (initialize) state, while
+ * 'r' (ready) state refers to a slot/origin created previously but already
+ * dropped. These states are supported for pg_upgrade. The other states listed
+ * below are not supported:
+ *
+ * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state would
+ * retain a replication slot, which could not be dropped by the sync worker
+ * spawned after the upgrade because the subscription ID used for the slot name
+ * won't match anymore.
+ *
+ * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state would
+ * retain the replication origin when there is a failure in tablesync worker
+ * immediately after dropping the replication slot in the publisher.
+ *
+ * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on a
+ * relation upgraded while in this state would expect an origin ID with the OID
+ * of the subscription used before the upgrade, causing it to fail.
+ *
+ * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and SUBREL_STATE_UNKNOWN:
+ * These states are not stored in the catalog, so we need not allow these
+ * states.
+ */
+static char *
+sub_query(void *arg)
+{
+	return pg_strdup("SELECT r.srsubstate, s.subname, n.nspname, c.relname "
+					 "FROM pg_catalog.pg_subscription_rel r "
+					 "LEFT JOIN pg_catalog.pg_subscription s"
+					 "   ON r.srsubid = s.oid "
+					 "LEFT JOIN pg_catalog.pg_class c"
+					 "   ON r.srrelid = c.oid "
+					 "LEFT JOIN pg_catalog.pg_namespace n"
+					 "   ON c.relnamespace = n.oid "
+					 "WHERE r.srsubstate NOT IN ('i', 'r') "
+					 "ORDER BY s.subname");
+}
+
+static void
+sub_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct substate_info *state = (struct substate_info *) arg;
+	int			ntup = PQntuples(res);
+	int			i_srsubstate = PQfnumber(res, "srsubstate");
+	int			i_subname = PQfnumber(res, "subname");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+
+	for (int i = 0; i < ntup; i++)
+	{
+		if (state->script == NULL &&
+			(state->script = fopen_priv(state->output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state->output_path);
+
+		fprintf(state->script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
+				PQgetvalue(res, i, i_srsubstate),
+				dbinfo->db_name,
+				PQgetvalue(res, i, i_subname),
+				PQgetvalue(res, i, i_nspname),
+				PQgetvalue(res, i, i_relname));
+	}
+}
+
 /*
  * check_old_cluster_subscription_state()
  *
@@ -1913,115 +1986,58 @@ check_old_cluster_for_valid_slots(void)
 static void
 check_old_cluster_subscription_state(void)
 {
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
+	struct substate_info state;
+	PGresult   *res;
+	PGconn	   *conn;
 	int			ntup;
 
 	prep_status("Checking for subscription state");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	state.script = NULL;
+	snprintf(state.output_path, sizeof(state.output_path), "%s/%s",
 			 log_opts.basedir,
 			 "subs_invalid.txt");
-	for (int dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
-
-		/* We need to check for pg_replication_origin only once. */
-		if (dbnum == 0)
-		{
-			/*
-			 * Check that all the subscriptions have their respective
-			 * replication origin.
-			 */
-			res = executeQueryOrDie(conn,
-									"SELECT d.datname, s.subname "
-									"FROM pg_catalog.pg_subscription s "
-									"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
-									"	ON o.roname = 'pg_' || s.oid "
-									"INNER JOIN pg_catalog.pg_database d "
-									"	ON d.oid = s.subdbid "
-									"WHERE o.roname IS NULL;");
-
-			ntup = PQntuples(res);
-			for (int i = 0; i < ntup; i++)
-			{
-				if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-					pg_fatal("could not open file \"%s\": %m", output_path);
-				fprintf(script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
-						PQgetvalue(res, i, 0),
-						PQgetvalue(res, i, 1));
-			}
-			PQclear(res);
-		}
 
-		/*
-		 * We don't allow upgrade if there is a risk of dangling slot or
-		 * origin corresponding to initial sync after upgrade.
-		 *
-		 * A slot/origin not created yet refers to the 'i' (initialize) state,
-		 * while 'r' (ready) state refers to a slot/origin created previously
-		 * but already dropped. These states are supported for pg_upgrade. The
-		 * other states listed below are not supported:
-		 *
-		 * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state
-		 * would retain a replication slot, which could not be dropped by the
-		 * sync worker spawned after the upgrade because the subscription ID
-		 * used for the slot name won't match anymore.
-		 *
-		 * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state
-		 * would retain the replication origin when there is a failure in
-		 * tablesync worker immediately after dropping the replication slot in
-		 * the publisher.
-		 *
-		 * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on
-		 * a relation upgraded while in this state would expect an origin ID
-		 * with the OID of the subscription used before the upgrade, causing
-		 * it to fail.
-		 *
-		 * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and
-		 * SUBREL_STATE_UNKNOWN: These states are not stored in the catalog,
-		 * so we need not allow these states.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT r.srsubstate, s.subname, n.nspname, c.relname "
-								"FROM pg_catalog.pg_subscription_rel r "
-								"LEFT JOIN pg_catalog.pg_subscription s"
-								"	ON r.srsubid = s.oid "
-								"LEFT JOIN pg_catalog.pg_class c"
-								"	ON r.srrelid = c.oid "
-								"LEFT JOIN pg_catalog.pg_namespace n"
-								"	ON c.relnamespace = n.oid "
-								"WHERE r.srsubstate NOT IN ('i', 'r') "
-								"ORDER BY s.subname");
-
-		ntup = PQntuples(res);
-		for (int i = 0; i < ntup; i++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
+	/*
+	 * Check that all the subscriptions have their respective replication
+	 * origin.  This check only needs to run once.
+	 */
+	conn = connectToServer(&old_cluster, old_cluster.dbarr.dbs[0].db_name);
+	res = executeQueryOrDie(conn,
+							"SELECT d.datname, s.subname "
+							"FROM pg_catalog.pg_subscription s "
+							"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
+							"	ON o.roname = 'pg_' || s.oid "
+							"INNER JOIN pg_catalog.pg_database d "
+							"	ON d.oid = s.subdbid "
+							"WHERE o.roname IS NULL;");
+	ntup = PQntuples(res);
+	for (int i = 0; i < ntup; i++)
+	{
+		if (state.script == NULL &&
+			(state.script = fopen_priv(state.output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state.output_path);
+		fprintf(state.script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
+				PQgetvalue(res, i, 0),
+				PQgetvalue(res, i, 1));
+	}
+	PQclear(res);
+	PQfinish(conn);
 
-			fprintf(script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
-					PQgetvalue(res, i, 0),
-					active_db->db_name,
-					PQgetvalue(res, i, 1),
-					PQgetvalue(res, i, 2),
-					PQgetvalue(res, i, 3));
-		}
+	async_task_add_step(task, sub_query, sub_process, true, &state);
 
-		PQclear(res);
-		PQfinish(conn);
-	}
+	async_task_run(task, &old_cluster);
+	async_task_free(task);
 
-	if (script)
+	if (state.script)
 	{
-		fclose(script);
+		fclose(state.script);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains subscriptions without origin or having relations not in i (initialize) or r (ready) state.\n"
 				 "You can allow the initial sync to finish for all relations and then restart the upgrade.\n"
 				 "A list of the problematic subscriptions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", state.output_path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v5-0005-use-new-pg_upgrade-async-API-for-retrieving-relin.patchtext/plain; charset=us-asciiDownload
From e62e03300dc2e207c9e0e22098fea6d93c9a909e Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:09:33 -0500
Subject: [PATCH v5 05/13] use new pg_upgrade async API for retrieving relinfos

---
 src/bin/pg_upgrade/info.c | 238 ++++++++++++++++++--------------------
 1 file changed, 110 insertions(+), 128 deletions(-)

diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index fc94de9ea4..798d96ef43 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -11,6 +11,7 @@
 
 #include "access/transam.h"
 #include "catalog/pg_class_d.h"
+#include "pqexpbuffer.h"
 #include "pg_upgrade.h"
 
 static void create_rel_filename_map(const char *old_data, const char *new_data,
@@ -22,13 +23,15 @@ static void report_unmatched_relation(const RelInfo *rel, const DbInfo *db,
 static void free_db_and_rel_infos(DbInfoArr *db_arr);
 static void get_template0_info(ClusterInfo *cluster);
 static void get_db_infos(ClusterInfo *cluster);
-static void get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo);
+static char *get_rel_infos_query(void *arg);
+static void get_rel_infos_result(DbInfo *dbinfo, PGresult *res, void *arg);
 static void free_rel_infos(RelInfoArr *rel_arr);
 static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
-static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo);
 static void get_subscription_count(ClusterInfo *cluster);
+static char *get_old_cluster_logical_slot_infos_query(void *arg);
+static void get_old_cluster_logical_slot_infos_result(DbInfo *dbinfo, PGresult *res, void *arg);
 
 
 /*
@@ -277,7 +280,7 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
 void
 get_db_rel_and_slot_infos(ClusterInfo *cluster)
 {
-	int			dbnum;
+	AsyncTask  *task = async_task_create();
 
 	if (cluster->dbarr.dbs != NULL)
 		free_db_and_rel_infos(&cluster->dbarr);
@@ -285,15 +288,19 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster)
 	get_template0_info(cluster);
 	get_db_infos(cluster);
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		DbInfo	   *pDbInfo = &cluster->dbarr.dbs[dbnum];
-
-		get_rel_infos(cluster, pDbInfo);
+	async_task_add_step(task,
+						get_rel_infos_query,
+						get_rel_infos_result,
+						true, NULL);
+	if (cluster == &old_cluster &&
+		GET_MAJOR_VERSION(cluster->major_version) > 1600)
+		async_task_add_step(task,
+							get_old_cluster_logical_slot_infos_query,
+							get_old_cluster_logical_slot_infos_result,
+							true, cluster);
 
-		if (cluster == &old_cluster)
-			get_old_cluster_logical_slot_infos(pDbInfo);
-	}
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (cluster == &old_cluster)
 		get_subscription_count(cluster);
@@ -443,32 +450,12 @@ get_db_infos(ClusterInfo *cluster)
  * Note: the resulting RelInfo array is assumed to be sorted by OID.
  * This allows later processing to match up old and new databases efficiently.
  */
-static void
-get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
+static char *
+get_rel_infos_query(void *arg)
 {
-	PGconn	   *conn = connectToServer(cluster,
-									   dbinfo->db_name);
-	PGresult   *res;
-	RelInfo    *relinfos;
-	int			ntups;
-	int			relnum;
-	int			num_rels = 0;
-	char	   *nspname = NULL;
-	char	   *relname = NULL;
-	char	   *tablespace = NULL;
-	int			i_spclocation,
-				i_nspname,
-				i_relname,
-				i_reloid,
-				i_indtable,
-				i_toastheap,
-				i_relfilenumber,
-				i_reltablespace;
-	char		query[QUERY_ALLOC];
-	char	   *last_namespace = NULL,
-			   *last_tablespace = NULL;
+	PQExpBufferData query;
 
-	query[0] = '\0';			/* initialize query string to empty */
+	initPQExpBuffer(&query);
 
 	/*
 	 * Create a CTE that collects OIDs of regular user tables and matviews,
@@ -480,34 +467,34 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * output, so we have to copy that system table.  It's easiest to do that
 	 * by treating it as a user table.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "WITH regular_heap (reloid, indtable, toastheap) AS ( "
-			 "  SELECT c.oid, 0::oid, 0::oid "
-			 "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
-			 "         ON c.relnamespace = n.oid "
-			 "  WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
-			 CppAsString2(RELKIND_MATVIEW) ") AND "
+	appendPQExpBuffer(&query,
+					  "WITH regular_heap (reloid, indtable, toastheap) AS ( "
+					  "  SELECT c.oid, 0::oid, 0::oid "
+					  "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
+					  "         ON c.relnamespace = n.oid "
+					  "  WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
+					  CppAsString2(RELKIND_MATVIEW) ") AND "
 	/* exclude possible orphaned temp tables */
-			 "    ((n.nspname !~ '^pg_temp_' AND "
-			 "      n.nspname !~ '^pg_toast_temp_' AND "
-			 "      n.nspname NOT IN ('pg_catalog', 'information_schema', "
-			 "                        'binary_upgrade', 'pg_toast') AND "
-			 "      c.oid >= %u::pg_catalog.oid) OR "
-			 "     (n.nspname = 'pg_catalog' AND "
-			 "      relname IN ('pg_largeobject') ))), ",
-			 FirstNormalObjectId);
+					  "    ((n.nspname !~ '^pg_temp_' AND "
+					  "      n.nspname !~ '^pg_toast_temp_' AND "
+					  "      n.nspname NOT IN ('pg_catalog', 'information_schema', "
+					  "                        'binary_upgrade', 'pg_toast') AND "
+					  "      c.oid >= %u::pg_catalog.oid) OR "
+					  "     (n.nspname = 'pg_catalog' AND "
+					  "      relname IN ('pg_largeobject') ))), ",
+					  FirstNormalObjectId);
 
 	/*
 	 * Add a CTE that collects OIDs of toast tables belonging to the tables
 	 * selected by the regular_heap CTE.  (We have to do this separately
 	 * because the namespace-name rules above don't work for toast tables.)
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "  toast_heap (reloid, indtable, toastheap) AS ( "
-			 "  SELECT c.reltoastrelid, 0::oid, c.oid "
-			 "  FROM regular_heap JOIN pg_catalog.pg_class c "
-			 "      ON regular_heap.reloid = c.oid "
-			 "  WHERE c.reltoastrelid != 0), ");
+	appendPQExpBufferStr(&query,
+						 "  toast_heap (reloid, indtable, toastheap) AS ( "
+						 "  SELECT c.reltoastrelid, 0::oid, c.oid "
+						 "  FROM regular_heap JOIN pg_catalog.pg_class c "
+						 "      ON regular_heap.reloid = c.oid "
+						 "  WHERE c.reltoastrelid != 0), ");
 
 	/*
 	 * Add a CTE that collects OIDs of all valid indexes on the previously
@@ -515,53 +502,61 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * Testing indisready is necessary in 9.2, and harmless in earlier/later
 	 * versions.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "  all_index (reloid, indtable, toastheap) AS ( "
-			 "  SELECT indexrelid, indrelid, 0::oid "
-			 "  FROM pg_catalog.pg_index "
-			 "  WHERE indisvalid AND indisready "
-			 "    AND indrelid IN "
-			 "        (SELECT reloid FROM regular_heap "
-			 "         UNION ALL "
-			 "         SELECT reloid FROM toast_heap)) ");
+	appendPQExpBufferStr(&query,
+						 "  all_index (reloid, indtable, toastheap) AS ( "
+						 "  SELECT indexrelid, indrelid, 0::oid "
+						 "  FROM pg_catalog.pg_index "
+						 "  WHERE indisvalid AND indisready "
+						 "    AND indrelid IN "
+						 "        (SELECT reloid FROM regular_heap "
+						 "         UNION ALL "
+						 "         SELECT reloid FROM toast_heap)) ");
 
 	/*
 	 * And now we can write the query that retrieves the data we want for each
 	 * heap and index relation.  Make sure result is sorted by OID.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "SELECT all_rels.*, n.nspname, c.relname, "
-			 "  c.relfilenode, c.reltablespace, "
-			 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
-			 "FROM (SELECT * FROM regular_heap "
-			 "      UNION ALL "
-			 "      SELECT * FROM toast_heap "
-			 "      UNION ALL "
-			 "      SELECT * FROM all_index) all_rels "
-			 "  JOIN pg_catalog.pg_class c "
-			 "      ON all_rels.reloid = c.oid "
-			 "  JOIN pg_catalog.pg_namespace n "
-			 "     ON c.relnamespace = n.oid "
-			 "  LEFT OUTER JOIN pg_catalog.pg_tablespace t "
-			 "     ON c.reltablespace = t.oid "
-			 "ORDER BY 1;");
-
-	res = executeQueryOrDie(conn, "%s", query);
-
-	ntups = PQntuples(res);
-
-	relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	appendPQExpBufferStr(&query,
+						 "SELECT all_rels.*, n.nspname, c.relname, "
+						 "  c.relfilenode, c.reltablespace, "
+						 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
+						 "FROM (SELECT * FROM regular_heap "
+						 "      UNION ALL "
+						 "      SELECT * FROM toast_heap "
+						 "      UNION ALL "
+						 "      SELECT * FROM all_index) all_rels "
+						 "  JOIN pg_catalog.pg_class c "
+						 "      ON all_rels.reloid = c.oid "
+						 "  JOIN pg_catalog.pg_namespace n "
+						 "     ON c.relnamespace = n.oid "
+						 "  LEFT OUTER JOIN pg_catalog.pg_tablespace t "
+						 "     ON c.reltablespace = t.oid "
+						 "ORDER BY 1;");
+
+	return query.data;
+}
 
-	i_reloid = PQfnumber(res, "reloid");
-	i_indtable = PQfnumber(res, "indtable");
-	i_toastheap = PQfnumber(res, "toastheap");
-	i_nspname = PQfnumber(res, "nspname");
-	i_relname = PQfnumber(res, "relname");
-	i_relfilenumber = PQfnumber(res, "relfilenode");
-	i_reltablespace = PQfnumber(res, "reltablespace");
-	i_spclocation = PQfnumber(res, "spclocation");
+static void
+get_rel_infos_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	int			ntups = PQntuples(res);
+	RelInfo    *relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	int			i_reloid = PQfnumber(res, "reloid");
+	int			i_indtable = PQfnumber(res, "indtable");
+	int			i_toastheap = PQfnumber(res, "toastheap");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+	int			i_relfilenumber = PQfnumber(res, "relfilenode");
+	int			i_reltablespace = PQfnumber(res, "reltablespace");
+	int			i_spclocation = PQfnumber(res, "spclocation");
+	int			num_rels = 0;
+	char	   *nspname = NULL;
+	char	   *relname = NULL;
+	char	   *tablespace = NULL;
+	char	   *last_namespace = NULL;
+	char	   *last_tablespace = NULL;
 
-	for (relnum = 0; relnum < ntups; relnum++)
+	for (int relnum = 0; relnum < ntups; relnum++)
 	{
 		RelInfo    *curr = &relinfos[num_rels++];
 
@@ -614,9 +609,6 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 			/* A zero reltablespace oid indicates the database tablespace. */
 			curr->tablespace = dbinfo->db_tablespace;
 	}
-	PQclear(res);
-
-	PQfinish(conn);
 
 	dbinfo->rel_arr.rels = relinfos;
 	dbinfo->rel_arr.nrels = num_rels;
@@ -638,20 +630,9 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
  * check_old_cluster_for_valid_slots() would raise a FATAL error if such slots
  * are included.
  */
-static void
-get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
+static char *
+get_old_cluster_logical_slot_infos_query(void *arg)
 {
-	PGconn	   *conn;
-	PGresult   *res;
-	LogicalSlotInfo *slotinfos = NULL;
-	int			num_slots;
-
-	/* Logical slots can be migrated since PG17. */
-	if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1600)
-		return;
-
-	conn = connectToServer(&old_cluster, dbinfo->db_name);
-
 	/*
 	 * Fetch the logical replication slot information. The check whether the
 	 * slot is considered caught up is done by an upgrade function. This
@@ -669,18 +650,23 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
-							"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
-							"FROM pg_catalog.pg_replication_slots "
-							"WHERE slot_type = 'logical' AND "
-							"database = current_database() AND "
-							"temporary IS FALSE;",
-							user_opts.live_check ? "FALSE" :
-							"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
-							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
-							"END)");
-
-	num_slots = PQntuples(res);
+	return psprintf("SELECT slot_name, plugin, two_phase, failover, "
+					"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
+					"FROM pg_catalog.pg_replication_slots "
+					"WHERE slot_type = 'logical' AND "
+					"database = current_database() AND "
+					"temporary IS FALSE;",
+					user_opts.live_check ? "FALSE" :
+					"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
+					"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
+					"END)");
+}
+
+static void
+get_old_cluster_logical_slot_infos_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	LogicalSlotInfo *slotinfos = NULL;
+	int			num_slots = PQntuples(res);
 
 	if (num_slots)
 	{
@@ -713,14 +699,10 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 		}
 	}
 
-	PQclear(res);
-	PQfinish(conn);
-
 	dbinfo->slot_arr.slots = slotinfos;
 	dbinfo->slot_arr.nslots = num_slots;
 }
 
-
 /*
  * count_old_cluster_logical_slots()
  *
-- 
2.39.3 (Apple Git-146)

v5-0006-use-new-pg_upgrade-async-API-to-parallelize-getti.patchtext/plain; charset=us-asciiDownload
From 1c822873b79171acb4ab50bf61d7a592b73dac09 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:24:35 -0500
Subject: [PATCH v5 06/13] use new pg_upgrade async API to parallelize getting
 loadable libraries

---
 src/bin/pg_upgrade/function.c | 63 ++++++++++++++++++++---------------
 1 file changed, 37 insertions(+), 26 deletions(-)

diff --git a/src/bin/pg_upgrade/function.c b/src/bin/pg_upgrade/function.c
index 7e3abed098..da3717e71c 100644
--- a/src/bin/pg_upgrade/function.c
+++ b/src/bin/pg_upgrade/function.c
@@ -42,6 +42,32 @@ library_name_compare(const void *p1, const void *p2)
 					  ((const LibraryInfo *) p2)->dbnum);
 }
 
+struct loadable_libraries_state
+{
+	PGresult  **ress;
+	int			totaltups;
+};
+
+static char *
+get_loadable_libraries_query(void *arg)
+{
+	return psprintf("SELECT DISTINCT probin "
+					"FROM pg_catalog.pg_proc "
+					"WHERE prolang = %u AND "
+					"probin IS NOT NULL AND "
+					"oid >= %u;",
+					ClanguageId,
+					FirstNormalObjectId);
+}
+
+static void
+get_loadable_libraries_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct loadable_libraries_state *state = (struct loadable_libraries_state *) arg;
+
+	state->ress[dbinfo - old_cluster.dbarr.dbs] = res;
+	state->totaltups += PQntuples(res);
+}
 
 /*
  * get_loadable_libraries()
@@ -54,47 +80,32 @@ library_name_compare(const void *p1, const void *p2)
 void
 get_loadable_libraries(void)
 {
-	PGresult  **ress;
 	int			totaltups;
 	int			dbnum;
 	int			n_libinfos;
+	AsyncTask  *task = async_task_create();
+	struct loadable_libraries_state state;
 
-	ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
-	totaltups = 0;
+	state.ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
+	state.totaltups = 0;
 
-	/* Fetch all library names, removing duplicates within each DB */
-	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
+	async_task_add_step(task, get_loadable_libraries_query,
+						get_loadable_libraries_result, false, &state);
 
-		/*
-		 * Fetch all libraries containing non-built-in C functions in this DB.
-		 */
-		ress[dbnum] = executeQueryOrDie(conn,
-										"SELECT DISTINCT probin "
-										"FROM pg_catalog.pg_proc "
-										"WHERE prolang = %u AND "
-										"probin IS NOT NULL AND "
-										"oid >= %u;",
-										ClanguageId,
-										FirstNormalObjectId);
-		totaltups += PQntuples(ress[dbnum]);
-
-		PQfinish(conn);
-	}
+	async_task_run(task, &old_cluster);
+	async_task_free(task);
 
 	/*
 	 * Allocate memory for required libraries and logical replication output
 	 * plugins.
 	 */
-	n_libinfos = totaltups + count_old_cluster_logical_slots();
+	n_libinfos = state.totaltups + count_old_cluster_logical_slots();
 	os_info.libraries = (LibraryInfo *) pg_malloc(sizeof(LibraryInfo) * n_libinfos);
 	totaltups = 0;
 
 	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
 	{
-		PGresult   *res = ress[dbnum];
+		PGresult   *res = state.ress[dbnum];
 		int			ntups;
 		int			rowno;
 		LogicalSlotInfoArr *slot_arr = &old_cluster.dbarr.dbs[dbnum].slot_arr;
@@ -129,7 +140,7 @@ get_loadable_libraries(void)
 		}
 	}
 
-	pg_free(ress);
+	pg_free(state.ress);
 
 	os_info.num_libraries = totaltups;
 }
-- 
2.39.3 (Apple Git-146)

v5-0007-use-new-pg_upgrade-async-API-to-parallelize-repor.patchtext/plain; charset=us-asciiDownload
From 274965e786fbaf369f01eb7ea24319b220580c7d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:31:57 -0500
Subject: [PATCH v5 07/13] use new pg_upgrade async API to parallelize
 reporting extension updates

---
 src/bin/pg_upgrade/version.c | 82 ++++++++++++++++++------------------
 1 file changed, 41 insertions(+), 41 deletions(-)

diff --git a/src/bin/pg_upgrade/version.c b/src/bin/pg_upgrade/version.c
index 2de6dffccd..bfcf08c936 100644
--- a/src/bin/pg_upgrade/version.c
+++ b/src/bin/pg_upgrade/version.c
@@ -139,6 +139,42 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 		check_ok();
 }
 
+static char *
+report_extension_updates_query(void *arg)
+{
+	return pg_strdup("SELECT name "
+					 "FROM pg_available_extensions "
+					 "WHERE installed_version != default_version");
+}
+
+static void
+report_extension_updates_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_name = PQfnumber(res, "name");
+	char	   *output_path = "update_extensions.sql";
+	FILE	  **script = (FILE **) arg;
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL && (*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			PQExpBufferData connectbuf;
+
+			initPQExpBuffer(&connectbuf);
+			appendPsqlMetaConnect(&connectbuf, dbinfo->db_name);
+			fputs(connectbuf.data, *script);
+			termPQExpBuffer(&connectbuf);
+			db_used = true;
+		}
+		fprintf(*script, "ALTER EXTENSION %s UPDATE;\n",
+				quote_identifier(PQgetvalue(res, rowno, i_name)));
+	}
+}
+
 /*
  * report_extension_updates()
  *	Report extensions that should be updated.
@@ -146,53 +182,17 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 void
 report_extension_updates(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char	   *output_path = "update_extensions.sql";
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for extension updates");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_name;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* find extensions needing updates */
-		res = executeQueryOrDie(conn,
-								"SELECT name "
-								"FROM pg_available_extensions "
-								"WHERE installed_version != default_version"
-			);
-
-		ntups = PQntuples(res);
-		i_name = PQfnumber(res, "name");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				PQExpBufferData connectbuf;
-
-				initPQExpBuffer(&connectbuf);
-				appendPsqlMetaConnect(&connectbuf, active_db->db_name);
-				fputs(connectbuf.data, script);
-				termPQExpBuffer(&connectbuf);
-				db_used = true;
-			}
-			fprintf(script, "ALTER EXTENSION %s UPDATE;\n",
-					quote_identifier(PQgetvalue(res, rowno, i_name)));
-		}
+	async_task_add_step(task, report_extension_updates_query,
+						report_extension_updates_result, true, &script);
 
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v5-0008-parallelize-data-type-checks-in-pg_upgrade.patchtext/plain; charset=us-asciiDownload
From c546c0106c0ff79e8bb74bdd4a902ab53f33f4f5 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Sat, 6 Jul 2024 21:06:31 -0500
Subject: [PATCH v5 08/13] parallelize data type checks in pg_upgrade

---
 src/bin/pg_upgrade/check.c | 319 +++++++++++++++++++------------------
 1 file changed, 160 insertions(+), 159 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 41a23e7efe..9877c7012f 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -32,6 +32,10 @@ static void check_new_cluster_subscription_configuration(void);
 static void check_old_cluster_for_valid_slots(void);
 static void check_old_cluster_subscription_state(void);
 
+static bool *data_type_check_results;
+static bool data_type_check_failed;
+static PQExpBufferData data_type_check_report;
+
 /*
  * DataTypesUsageChecks - definitions of data type checks for the old cluster
  * in order to determine if an upgrade can be performed.  See the comment on
@@ -314,6 +318,129 @@ static DataTypesUsageChecks data_types_usage_checks[] =
 	}
 };
 
+static char *
+data_type_check_query(void *arg)
+{
+	int			i = (int) (intptr_t) arg;
+	DataTypesUsageChecks *check = &data_types_usage_checks[i];
+
+	return psprintf("WITH RECURSIVE oids AS ( "
+	/* start with the type(s) returned by base_query */
+					"	%s "
+					"	UNION ALL "
+					"	SELECT * FROM ( "
+	/* inner WITH because we can only reference the CTE once */
+					"		WITH x AS (SELECT oid FROM oids) "
+	/* domains on any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
+					"			UNION ALL "
+	/* arrays over any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
+					"			UNION ALL "
+	/* composite types containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
+					"			WHERE t.typtype = 'c' AND "
+					"				  t.oid = c.reltype AND "
+					"				  c.oid = a.attrelid AND "
+					"				  NOT a.attisdropped AND "
+					"				  a.atttypid = x.oid "
+					"			UNION ALL "
+	/* ranges containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
+					"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
+					"	) foo "
+					") "
+	/* now look for stored columns of any such type */
+					"SELECT n.nspname, c.relname, a.attname "
+					"FROM	pg_catalog.pg_class c, "
+					"		pg_catalog.pg_namespace n, "
+					"		pg_catalog.pg_attribute a "
+					"WHERE	c.oid = a.attrelid AND "
+					"		NOT a.attisdropped AND "
+					"		a.atttypid IN (SELECT oid FROM oids) AND "
+					"		c.relkind IN ("
+					CppAsString2(RELKIND_RELATION) ", "
+					CppAsString2(RELKIND_MATVIEW) ", "
+					CppAsString2(RELKIND_INDEX) ") AND "
+					"		c.relnamespace = n.oid AND "
+	/* exclude possible orphaned temp tables */
+					"		n.nspname !~ '^pg_temp_' AND "
+					"		n.nspname !~ '^pg_toast_temp_' AND "
+	/* exclude system catalogs, too */
+					"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
+					check->base_query);
+}
+
+static void
+data_type_check_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	int			i = (int) (intptr_t) arg;
+	DataTypesUsageChecks *check = &data_types_usage_checks[i];
+	int			ntups = PQntuples(res);
+
+	if (ntups)
+	{
+		char		output_path[MAXPGPATH];
+		int			i_nspname;
+		int			i_relname;
+		int			i_attname;
+		FILE	   *script = NULL;
+		bool		db_used = false;
+
+		snprintf(output_path, sizeof(output_path), "%s/%s",
+				 log_opts.basedir,
+				 check->report_filename);
+
+		/*
+		 * Make sure we have a buffer to save reports to now that we found a
+		 * first failing check.
+		 */
+		if (!data_type_check_failed)
+			initPQExpBuffer(&data_type_check_report);
+		data_type_check_failed = true;
+
+		/*
+		 * If this is the first time we see an error for the check in question
+		 * then print a status message of the failure.
+		 */
+		if (!data_type_check_results[i])
+		{
+			pg_log(PG_REPORT, "    failed check: %s", _(check->status));
+			appendPQExpBuffer(&data_type_check_report, "\n%s\n%s    %s\n",
+							  _(check->report_text),
+							  _("A list of the problem columns is in the file:"),
+							  output_path);
+		}
+		data_type_check_results[i] = true;
+
+		i_nspname = PQfnumber(res, "nspname");
+		i_relname = PQfnumber(res, "relname");
+		i_attname = PQfnumber(res, "attname");
+
+		for (int rowno = 0; rowno < ntups; rowno++)
+		{
+			if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
+				pg_fatal("could not open file \"%s\": %m", output_path);
+
+			if (!db_used)
+			{
+				fprintf(script, "In database: %s\n", dbinfo->db_name);
+				db_used = true;
+			}
+			fprintf(script, "  %s.%s.%s\n",
+					PQgetvalue(res, rowno, i_nspname),
+					PQgetvalue(res, rowno, i_relname),
+					PQgetvalue(res, rowno, i_attname));
+		}
+
+		if (script)
+		{
+			fclose(script);
+			script = NULL;
+		}
+	}
+}
+
 /*
  * check_for_data_types_usage()
  *	Detect whether there are any stored columns depending on given type(s)
@@ -337,10 +464,9 @@ static void
 check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
 {
 	bool		found = false;
-	bool	   *results;
-	PQExpBufferData report;
 	DataTypesUsageChecks *tmp = checks;
 	int			n_data_types_usage_checks = 0;
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for data type usage");
 
@@ -352,176 +478,51 @@ check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
 	}
 
 	/* Prepare an array to store the results of checks in */
-	results = pg_malloc0(sizeof(bool) * n_data_types_usage_checks);
+	data_type_check_results = pg_malloc0(sizeof(bool) * n_data_types_usage_checks);
+	data_type_check_failed = false;
 
-	/*
-	 * Connect to each database in the cluster and run all defined checks
-	 * against that database before trying the next one.
-	 */
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int i = 0; i < n_data_types_usage_checks; i++)
 	{
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
+		DataTypesUsageChecks *check = &checks[i];
 
-		for (int checknum = 0; checknum < n_data_types_usage_checks; checknum++)
+		if (check->threshold_version == MANUAL_CHECK)
 		{
-			PGresult   *res;
-			int			ntups;
-			int			i_nspname;
-			int			i_relname;
-			int			i_attname;
-			FILE	   *script = NULL;
-			bool		db_used = false;
-			char		output_path[MAXPGPATH];
-			DataTypesUsageChecks *cur_check = &checks[checknum];
-
-			if (cur_check->threshold_version == MANUAL_CHECK)
-			{
-				Assert(cur_check->version_hook);
-
-				/*
-				 * Make sure that the check applies to the current cluster
-				 * version and skip if not. If no check hook has been defined
-				 * we run the check for all versions.
-				 */
-				if (!cur_check->version_hook(cluster))
-					continue;
-			}
-			else if (cur_check->threshold_version != ALL_VERSIONS)
-			{
-				if (GET_MAJOR_VERSION(cluster->major_version) > cur_check->threshold_version)
-					continue;
-			}
-			else
-				Assert(cur_check->threshold_version == ALL_VERSIONS);
-
-			snprintf(output_path, sizeof(output_path), "%s/%s",
-					 log_opts.basedir,
-					 cur_check->report_filename);
-
-			/*
-			 * The type(s) of interest might be wrapped in a domain, array,
-			 * composite, or range, and these container types can be nested
-			 * (to varying extents depending on server version, but that's not
-			 * of concern here).  To handle all these cases we need a
-			 * recursive CTE.
-			 */
-			res = executeQueryOrDie(conn,
-									"WITH RECURSIVE oids AS ( "
-			/* start with the type(s) returned by base_query */
-									"	%s "
-									"	UNION ALL "
-									"	SELECT * FROM ( "
-			/* inner WITH because we can only reference the CTE once */
-									"		WITH x AS (SELECT oid FROM oids) "
-			/* domains on any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
-									"			UNION ALL "
-			/* arrays over any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
-									"			UNION ALL "
-			/* composite types containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
-									"			WHERE t.typtype = 'c' AND "
-									"				  t.oid = c.reltype AND "
-									"				  c.oid = a.attrelid AND "
-									"				  NOT a.attisdropped AND "
-									"				  a.atttypid = x.oid "
-									"			UNION ALL "
-			/* ranges containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
-									"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
-									"	) foo "
-									") "
-			/* now look for stored columns of any such type */
-									"SELECT n.nspname, c.relname, a.attname "
-									"FROM	pg_catalog.pg_class c, "
-									"		pg_catalog.pg_namespace n, "
-									"		pg_catalog.pg_attribute a "
-									"WHERE	c.oid = a.attrelid AND "
-									"		NOT a.attisdropped AND "
-									"		a.atttypid IN (SELECT oid FROM oids) AND "
-									"		c.relkind IN ("
-									CppAsString2(RELKIND_RELATION) ", "
-									CppAsString2(RELKIND_MATVIEW) ", "
-									CppAsString2(RELKIND_INDEX) ") AND "
-									"		c.relnamespace = n.oid AND "
-			/* exclude possible orphaned temp tables */
-									"		n.nspname !~ '^pg_temp_' AND "
-									"		n.nspname !~ '^pg_toast_temp_' AND "
-			/* exclude system catalogs, too */
-									"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
-									cur_check->base_query);
-
-			ntups = PQntuples(res);
+			Assert(check->version_hook);
 
 			/*
-			 * The datatype was found, so extract the data and log to the
-			 * requested filename. We need to open the file for appending
-			 * since the check might have already found the type in another
-			 * database earlier in the loop.
+			 * Make sure that the check applies to the current cluster version
+			 * and skip it if not.
 			 */
-			if (ntups)
-			{
-				/*
-				 * Make sure we have a buffer to save reports to now that we
-				 * found a first failing check.
-				 */
-				if (!found)
-					initPQExpBuffer(&report);
-				found = true;
-
-				/*
-				 * If this is the first time we see an error for the check in
-				 * question then print a status message of the failure.
-				 */
-				if (!results[checknum])
-				{
-					pg_log(PG_REPORT, "    failed check: %s", _(cur_check->status));
-					appendPQExpBuffer(&report, "\n%s\n%s    %s\n",
-									  _(cur_check->report_text),
-									  _("A list of the problem columns is in the file:"),
-									  output_path);
-				}
-				results[checknum] = true;
-
-				i_nspname = PQfnumber(res, "nspname");
-				i_relname = PQfnumber(res, "relname");
-				i_attname = PQfnumber(res, "attname");
-
-				for (int rowno = 0; rowno < ntups; rowno++)
-				{
-					if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
-						pg_fatal("could not open file \"%s\": %m", output_path);
-
-					if (!db_used)
-					{
-						fprintf(script, "In database: %s\n", active_db->db_name);
-						db_used = true;
-					}
-					fprintf(script, "  %s.%s.%s\n",
-							PQgetvalue(res, rowno, i_nspname),
-							PQgetvalue(res, rowno, i_relname),
-							PQgetvalue(res, rowno, i_attname));
-				}
-
-				if (script)
-				{
-					fclose(script);
-					script = NULL;
-				}
-			}
-
-			PQclear(res);
+			if (!check->version_hook(cluster))
+				continue;
 		}
+		else if (check->threshold_version != ALL_VERSIONS)
+		{
+			if (GET_MAJOR_VERSION(cluster->major_version) > check->threshold_version)
+				continue;
+		}
+		else
+			Assert(check->threshold_version == ALL_VERSIONS);
 
-		PQfinish(conn);
+		async_task_add_step(task, data_type_check_query,
+							data_type_check_process, true,
+							(void *) (intptr_t) i);
 	}
 
+	/*
+	 * Connect to each database in the cluster and run all defined checks
+	 * against that database before trying the next one.
+	 */
+	async_task_run(task, cluster);
+	async_task_free(task);
+
 	if (found)
-		pg_fatal("Data type checks failed: %s", report.data);
+	{
+		pg_fatal("Data type checks failed: %s", data_type_check_report.data);
+		termPQExpBuffer(&data_type_check_report);
+	}
 
-	pg_free(results);
+	pg_free(data_type_check_results);
 
 	check_ok();
 }
-- 
2.39.3 (Apple Git-146)

v5-0009-parallelize-isn-and-int8-passing-mismatch-check-i.patchtext/plain; charset=us-asciiDownload
From b882aa11fb05c4985934e45496e7c374c8bf2461 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:00:20 -0500
Subject: [PATCH v5 09/13] parallelize isn and int8 passing mismatch check in
 pg_upgrade

---
 src/bin/pg_upgrade/check.c | 85 ++++++++++++++++++++------------------
 1 file changed, 44 insertions(+), 41 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 9877c7012f..505ebd1caa 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1193,6 +1193,44 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 	check_ok();
 }
 
+static char *
+isn_and_int8_passing_mismatch_query(void *arg)
+{
+	return pg_strdup("SELECT n.nspname, p.proname "
+					 "FROM   pg_catalog.pg_proc p, "
+					 "       pg_catalog.pg_namespace n "
+					 "WHERE  p.pronamespace = n.oid AND "
+					 "       p.probin = '$libdir/isn'");
+}
+
+static void
+isn_and_int8_passing_mismatch_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_proname = PQfnumber(res, "proname");
+	FILE	  **script = (FILE **) arg;
+	char		output_path[MAXPGPATH];
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "contrib_isn_and_int8_pass_by_value.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL && (*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_proname));
+	}
+}
 
 /*
  *	check_for_isn_and_int8_passing_mismatch()
@@ -1204,8 +1242,8 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 static void
 check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
+	AsyncTask  *task;
 	char		output_path[MAXPGPATH];
 
 	prep_status("Checking for contrib/isn with bigint-passing mismatch");
@@ -1222,46 +1260,11 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "contrib_isn_and_int8_pass_by_value.txt");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_proname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* Find any functions coming from contrib/isn */
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, p.proname "
-								"FROM	pg_catalog.pg_proc p, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	p.pronamespace = n.oid AND "
-								"		p.probin = '$libdir/isn'");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_proname = PQfnumber(res, "proname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_proname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	task = async_task_create();
+	async_task_add_step(task, isn_and_int8_passing_mismatch_query,
+						isn_and_int8_passing_mismatch_process, true, &script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v5-0010-parallelize-user-defined-postfix-ops-check-in-pg_.patchtext/plain; charset=us-asciiDownload
From 79c3f7b4303b6e7a857b02f2194c06cb92d42301 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:12:49 -0500
Subject: [PATCH v5 10/13] parallelize user defined postfix ops check in
 pg_upgrade

---
 src/bin/pg_upgrade/check.c | 134 +++++++++++++++++++------------------
 1 file changed, 69 insertions(+), 65 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 505ebd1caa..664039b441 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1282,15 +1282,79 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 		check_ok();
 }
 
+static char *
+user_defined_postfix_ops_query(void *arg)
+{
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	return pg_strdup("SELECT o.oid AS oproid, "
+					 "       n.nspname AS oprnsp, "
+					 "       o.oprname, "
+					 "       tn.nspname AS typnsp, "
+					 "       t.typname "
+					 "FROM pg_catalog.pg_operator o, "
+					 "     pg_catalog.pg_namespace n, "
+					 "     pg_catalog.pg_type t, "
+					 "     pg_catalog.pg_namespace tn "
+					 "WHERE o.oprnamespace = n.oid AND "
+					 "      o.oprleft = t.oid AND "
+					 "      t.typnamespace = tn.oid AND "
+					 "      o.oprright = 0 AND "
+					 "      o.oid >= 16384");
+}
+
+static void
+user_defined_postfix_ops_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	FILE	  **script = (FILE **) arg;
+	char		output_path[MAXPGPATH];
+	int			ntups = PQntuples(res);
+	bool		db_used = false;
+	int			i_oproid = PQfnumber(res, "oproid");
+	int			i_oprnsp = PQfnumber(res, "oprnsp");
+	int			i_oprname = PQfnumber(res, "oprname");
+	int			i_typnsp = PQfnumber(res, "typnsp");
+	int			i_typname = PQfnumber(res, "typname");
+
+	if (!ntups)
+		return;
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "postfix_ops.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL &&
+			(*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
+				PQgetvalue(res, rowno, i_oproid),
+				PQgetvalue(res, rowno, i_oprnsp),
+				PQgetvalue(res, rowno, i_oprname),
+				PQgetvalue(res, rowno, i_typnsp),
+				PQgetvalue(res, rowno, i_typname));
+	}
+}
+
 /*
  * Verify that no user defined postfix operators exist.
  */
 static void
 check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for user-defined postfix operators");
 
@@ -1298,70 +1362,10 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "postfix_ops.txt");
 
-	/* Find any user defined postfix operators */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_oproid,
-					i_oprnsp,
-					i_oprname,
-					i_typnsp,
-					i_typname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT o.oid AS oproid, "
-								"       n.nspname AS oprnsp, "
-								"       o.oprname, "
-								"       tn.nspname AS typnsp, "
-								"       t.typname "
-								"FROM pg_catalog.pg_operator o, "
-								"     pg_catalog.pg_namespace n, "
-								"     pg_catalog.pg_type t, "
-								"     pg_catalog.pg_namespace tn "
-								"WHERE o.oprnamespace = n.oid AND "
-								"      o.oprleft = t.oid AND "
-								"      t.typnamespace = tn.oid AND "
-								"      o.oprright = 0 AND "
-								"      o.oid >= 16384");
-		ntups = PQntuples(res);
-		i_oproid = PQfnumber(res, "oproid");
-		i_oprnsp = PQfnumber(res, "oprnsp");
-		i_oprname = PQfnumber(res, "oprname");
-		i_typnsp = PQfnumber(res, "typnsp");
-		i_typname = PQfnumber(res, "typname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
-					PQgetvalue(res, rowno, i_oproid),
-					PQgetvalue(res, rowno, i_oprnsp),
-					PQgetvalue(res, rowno, i_oprname),
-					PQgetvalue(res, rowno, i_typnsp),
-					PQgetvalue(res, rowno, i_typname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_add_step(task, user_defined_postfix_ops_query,
+						user_defined_postfix_ops_process, true, &script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v5-0011-parallelize-incompatible-polymorphics-check-in-pg.patchtext/plain; charset=us-asciiDownload
From 3ee389a9ffebf05fb691beb16b0e0a5f94d02f9b Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:30:19 -0500
Subject: [PATCH v5 11/13] parallelize incompatible polymorphics check in
 pg_upgrade

---
 src/bin/pg_upgrade/check.c       | 174 ++++++++++++++++---------------
 src/tools/pgindent/typedefs.list |   1 +
 2 files changed, 92 insertions(+), 83 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 664039b441..614464dfd3 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1381,6 +1381,81 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 		check_ok();
 }
 
+typedef struct incompat_polymorphics_state
+{
+	FILE	   *script;
+	PQExpBufferData old_polymorphics;
+	char		output_path[MAXPGPATH];
+} incompat_polymorphics_state;
+
+static char *
+incompat_polymorphics_query(void *arg)
+{
+	incompat_polymorphics_state *state = (incompat_polymorphics_state *) arg;
+
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	/* Aggregate transition functions */
+	return psprintf("SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					"FROM pg_proc AS p "
+					"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					"JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
+					"WHERE p.oid >= 16384 "
+					"AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
+					"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Aggregate final functions */
+					"UNION ALL "
+					"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					"FROM pg_proc AS p "
+					"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					"JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
+					"WHERE p.oid >= 16384 "
+					"AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
+					"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Operators */
+					"UNION ALL "
+					"SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
+					"FROM pg_operator AS op "
+					"WHERE op.oid >= 16384 "
+					"AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
+					"AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
+					state->old_polymorphics.data,
+					state->old_polymorphics.data,
+					state->old_polymorphics.data);
+}
+
+static void
+incompat_polymorphics_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	incompat_polymorphics_state *state = (incompat_polymorphics_state *) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_objkind = PQfnumber(res, "objkind");
+	int			i_objname = PQfnumber(res, "objname");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (state->script == NULL &&
+			(state->script = fopen_priv(state->output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state->output_path);
+		if (!db_used)
+		{
+			fprintf(state->script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+
+		fprintf(state->script, "  %s: %s\n",
+				PQgetvalue(res, rowno, i_objkind),
+				PQgetvalue(res, rowno, i_objname));
+	}
+}
+
 /*
  *	check_for_incompatible_polymorphics()
  *
@@ -1390,111 +1465,44 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 static void
 check_for_incompatible_polymorphics(ClusterInfo *cluster)
 {
-	PGresult   *res;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
-	PQExpBufferData old_polymorphics;
+	AsyncTask  *task = async_task_create();
+	incompat_polymorphics_state state;
 
 	prep_status("Checking for incompatible polymorphic functions");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	state.script = NULL;
+	snprintf(state.output_path, sizeof(state.output_path), "%s/%s",
 			 log_opts.basedir,
 			 "incompatible_polymorphics.txt");
 
 	/* The set of problematic functions varies a bit in different versions */
-	initPQExpBuffer(&old_polymorphics);
+	initPQExpBuffer(&state.old_polymorphics);
 
-	appendPQExpBufferStr(&old_polymorphics,
+	appendPQExpBufferStr(&state.old_polymorphics,
 						 "'array_append(anyarray,anyelement)'"
 						 ", 'array_cat(anyarray,anyarray)'"
 						 ", 'array_prepend(anyelement,anyarray)'");
 
 	if (GET_MAJOR_VERSION(cluster->major_version) >= 903)
-		appendPQExpBufferStr(&old_polymorphics,
+		appendPQExpBufferStr(&state.old_polymorphics,
 							 ", 'array_remove(anyarray,anyelement)'"
 							 ", 'array_replace(anyarray,anyelement,anyelement)'");
 
 	if (GET_MAJOR_VERSION(cluster->major_version) >= 905)
-		appendPQExpBufferStr(&old_polymorphics,
+		appendPQExpBufferStr(&state.old_polymorphics,
 							 ", 'array_position(anyarray,anyelement)'"
 							 ", 'array_position(anyarray,anyelement,integer)'"
 							 ", 'array_positions(anyarray,anyelement)'"
 							 ", 'width_bucket(anyelement,anyarray)'");
 
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		bool		db_used = false;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-		int			ntups;
-		int			i_objkind,
-					i_objname;
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-		/* Aggregate transition functions */
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Aggregate final functions */
-								"UNION ALL "
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Operators */
-								"UNION ALL "
-								"SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
-								"FROM pg_operator AS op "
-								"WHERE op.oid >= 16384 "
-								"AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
-								old_polymorphics.data,
-								old_polymorphics.data,
-								old_polymorphics.data);
-
-		ntups = PQntuples(res);
-
-		i_objkind = PQfnumber(res, "objkind");
-		i_objname = PQfnumber(res, "objname");
-
-		for (int rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-
-			fprintf(script, "  %s: %s\n",
-					PQgetvalue(res, rowno, i_objkind),
-					PQgetvalue(res, rowno, i_objname));
-		}
-
-		PQclear(res);
-		PQfinish(conn);
-	}
+	async_task_add_step(task, incompat_polymorphics_query,
+						incompat_polymorphics_process, true, &state);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
-	if (script)
+	if (state.script)
 	{
-		fclose(script);
+		fclose(state.script);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains user-defined objects that refer to internal\n"
 				 "polymorphic functions with arguments of type \"anyarray\" or \"anyelement\".\n"
@@ -1502,12 +1510,12 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 				 "afterwards, changing them to refer to the new corresponding functions with\n"
 				 "arguments of type \"anycompatiblearray\" and \"anycompatible\".\n"
 				 "A list of the problematic objects is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", state.output_path);
 	}
 	else
 		check_ok();
 
-	termPQExpBuffer(&old_polymorphics);
+	termPQExpBuffer(&state.old_polymorphics);
 }
 
 /*
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a2330f4346..66db3d3e2c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3536,6 +3536,7 @@ hstoreUpgrade_t
 hyperLogLogState
 ifState
 import_error_callback_arg
+incompat_polymorphics_state
 indexed_tlist
 inet
 inetKEY
-- 
2.39.3 (Apple Git-146)

v5-0012-parallelize-tables-with-oids-check-in-pg_upgrade.patchtext/plain; charset=us-asciiDownload
From 20d8f0905226a2e2d05deff83054f7ba7561d5f1 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:42:22 -0500
Subject: [PATCH v5 12/13] parallelize tables with oids check in pg_upgrade

---
 src/bin/pg_upgrade/check.c | 90 ++++++++++++++++++++------------------
 1 file changed, 48 insertions(+), 42 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 614464dfd3..a09eafa16e 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1518,15 +1518,58 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 	termPQExpBuffer(&state.old_polymorphics);
 }
 
+static char *
+with_oids_query(void *arg)
+{
+	return pg_strdup("SELECT n.nspname, c.relname "
+					 "FROM   pg_catalog.pg_class c, "
+					 "       pg_catalog.pg_namespace n "
+					 "WHERE  c.relnamespace = n.oid AND "
+					 "       c.relhasoids AND"
+					 "       n.nspname NOT IN ('pg_catalog')");
+}
+
+static void
+with_oids_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	char		output_path[MAXPGPATH];
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+	FILE	  **script = (FILE **) arg;
+
+	if (!ntups)
+		return;
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "tables_with_oids.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL && (*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_relname));
+	}
+}
+
 /*
  * Verify that no tables are declared WITH OIDS.
  */
 static void
 check_for_tables_with_oids(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for tables WITH OIDS");
 
@@ -1534,47 +1577,10 @@ check_for_tables_with_oids(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "tables_with_oids.txt");
 
-	/* Find any tables declared WITH OIDS */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_relname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, c.relname "
-								"FROM	pg_catalog.pg_class c, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	c.relnamespace = n.oid AND "
-								"		c.relhasoids AND"
-								"       n.nspname NOT IN ('pg_catalog')");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_relname = PQfnumber(res, "relname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_relname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_add_step(task, with_oids_query,
+						with_oids_process, true, &script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v5-0013-parallelize-user-defined-encoding-conversions-che.patchtext/plain; charset=us-asciiDownload
From 86486c2c8e1862ad616967efa7aa025aa60c9283 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:52:13 -0500
Subject: [PATCH v5 13/13] parallelize user defined encoding conversions check
 in pg_upgrade

---
 src/bin/pg_upgrade/check.c | 107 ++++++++++++++++++++-----------------
 1 file changed, 57 insertions(+), 50 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index a09eafa16e..5ef8169488 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1654,15 +1654,66 @@ check_for_pg_role_prefix(ClusterInfo *cluster)
 		check_ok();
 }
 
+static char *
+user_defined_encoding_conversions_query(void *arg)
+{
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	return pstrdup("SELECT c.oid as conoid, c.conname, n.nspname "
+				   "FROM pg_catalog.pg_conversion c, "
+				   "     pg_catalog.pg_namespace n "
+				   "WHERE c.connamespace = n.oid AND "
+				   "      c.oid >= 16384");
+}
+
+static void
+user_defined_encoding_conversions_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	FILE	  **script = (FILE **) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	char		output_path[MAXPGPATH];
+	int			i_conoid = PQfnumber(res, "conoid");
+	int			i_conname = PQfnumber(res, "conname");
+	int			i_nspname = PQfnumber(res, "nspname");
+
+	if (!ntups)
+		return;
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "encoding_conversions.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL &&
+			(*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  (oid=%s) %s.%s\n",
+				PQgetvalue(res, rowno, i_conoid),
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_conname));
+	}
+}
+
 /*
  * Verify that no user-defined encoding conversions exist.
  */
 static void
 check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for user-defined encoding conversions");
 
@@ -1670,55 +1721,11 @@ check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "encoding_conversions.txt");
 
-	/* Find any user defined encoding conversions */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_conoid,
-					i_conname,
-					i_nspname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT c.oid as conoid, c.conname, n.nspname "
-								"FROM pg_catalog.pg_conversion c, "
-								"     pg_catalog.pg_namespace n "
-								"WHERE c.connamespace = n.oid AND "
-								"      c.oid >= 16384");
-		ntups = PQntuples(res);
-		i_conoid = PQfnumber(res, "conoid");
-		i_conname = PQfnumber(res, "conname");
-		i_nspname = PQfnumber(res, "nspname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s\n",
-					PQgetvalue(res, rowno, i_conoid),
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_conname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_add_step(task, user_defined_encoding_conversions_query,
+						user_defined_encoding_conversions_process, true,
+						&script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

#14Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#13)
1 attachment(s)
Re: optimizing pg_upgrade's once-in-each-database steps

On Mon, Jul 22, 2024 at 03:07:10PM -0500, Nathan Bossart wrote:

Here is a new patch set. I've included the latest revision of the patch to
fix get_db_subscription_count() from the other thread [0] as 0001 since I
expect that to be committed soon. I've also moved the patch that moves the
"live_check" variable to "user_opts" to 0002 since I plan on committing
that sooner than later, too. Otherwise, I've tried to address all feedback
provided thus far.

Here is just the live_check patch. I rebased it, gave it a commit message,
and fixed a silly mistake. Barring objections or cfbot indigestion, I plan
to commit this within the next couple of days.

--
nathan

Attachments:

v6-0001-pg_upgrade-Move-live_check-variable-to-user_opts.patchtext/plain; charset=us-asciiDownload
From 963ca637e9b2481eb762de35b4d4eaa30faf5f47 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 24 Jul 2024 21:55:15 -0500
Subject: [PATCH v6 1/1] pg_upgrade: Move live_check variable to user_opts.

At the moment, pg_upgrade stores whether it is doing a "live check"
(i.e., the user specified --check and the old server is still
running) in a variable scoped to main().  This live_check variable
is passed to several functions.  To further complicate matters, a
few calls to said functions provide a hard-coded "false" as the
live_check argument.  Specifically, this is done when calling these
functions for the new cluster, for which any live-check-only paths
won't apply.

This commit moves the live_check variable to the global user_opts
variable, which stores information about the options the user
provided on the command line.  This allows us to remove the
"live_check" parameter from several functions.  For the functions
with callers that provide a hard-coded "false" as the live_check
argument (e.g., get_control_data()), we simply verify the given
cluster is the old cluster before taking any live-check-only paths.

This small refactoring effort aims to simplify proposed future
commits that would parallelize many of pg_upgrade's
once-in-each-database tasks using libpq's asynchronous APIs.
Specifically, by removing the live_check parameter, we can more
easily convert the functions to callbacks for the new parallel
system.

Reviewed-by: Daniel Gustafsson
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c       | 30 +++++++++++++++---------------
 src/bin/pg_upgrade/controldata.c |  3 ++-
 src/bin/pg_upgrade/info.c        | 12 +++++-------
 src/bin/pg_upgrade/option.c      |  4 ++--
 src/bin/pg_upgrade/pg_upgrade.c  | 21 ++++++++++-----------
 src/bin/pg_upgrade/pg_upgrade.h  | 13 +++++++------
 6 files changed, 41 insertions(+), 42 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 51e30a2f23..5038231731 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -29,7 +29,7 @@ static void check_for_new_tablespace_dir(void);
 static void check_for_user_defined_encoding_conversions(ClusterInfo *cluster);
 static void check_new_cluster_logical_replication_slots(void);
 static void check_new_cluster_subscription_configuration(void);
-static void check_old_cluster_for_valid_slots(bool live_check);
+static void check_old_cluster_for_valid_slots(void);
 static void check_old_cluster_subscription_state(void);
 
 /*
@@ -555,9 +555,9 @@ fix_path_separator(char *path)
 }
 
 void
-output_check_banner(bool live_check)
+output_check_banner(void)
 {
-	if (user_opts.check && live_check)
+	if (user_opts.live_check)
 	{
 		pg_log(PG_REPORT,
 			   "Performing Consistency Checks on Old Live Server\n"
@@ -573,18 +573,18 @@ output_check_banner(bool live_check)
 
 
 void
-check_and_dump_old_cluster(bool live_check)
+check_and_dump_old_cluster(void)
 {
 	/* -- OLD -- */
 
-	if (!live_check)
+	if (!user_opts.live_check)
 		start_postmaster(&old_cluster, true);
 
 	/*
 	 * Extract a list of databases, tables, and logical replication slots from
 	 * the old cluster.
 	 */
-	get_db_rel_and_slot_infos(&old_cluster, live_check);
+	get_db_rel_and_slot_infos(&old_cluster);
 
 	init_tablespaces();
 
@@ -605,7 +605,7 @@ check_and_dump_old_cluster(bool live_check)
 		 * Logical replication slots can be migrated since PG17. See comments
 		 * atop get_old_cluster_logical_slot_infos().
 		 */
-		check_old_cluster_for_valid_slots(live_check);
+		check_old_cluster_for_valid_slots();
 
 		/*
 		 * Subscriptions and their dependencies can be migrated since PG17.
@@ -669,7 +669,7 @@ check_and_dump_old_cluster(bool live_check)
 	if (!user_opts.check)
 		generate_old_dump();
 
-	if (!live_check)
+	if (!user_opts.live_check)
 		stop_postmaster(false);
 }
 
@@ -677,7 +677,7 @@ check_and_dump_old_cluster(bool live_check)
 void
 check_new_cluster(void)
 {
-	get_db_rel_and_slot_infos(&new_cluster, false);
+	get_db_rel_and_slot_infos(&new_cluster);
 
 	check_new_cluster_is_empty();
 
@@ -828,14 +828,14 @@ check_cluster_versions(void)
 
 
 void
-check_cluster_compatibility(bool live_check)
+check_cluster_compatibility(void)
 {
 	/* get/check pg_control data of servers */
-	get_control_data(&old_cluster, live_check);
-	get_control_data(&new_cluster, false);
+	get_control_data(&old_cluster);
+	get_control_data(&new_cluster);
 	check_control_data(&old_cluster.controldata, &new_cluster.controldata);
 
-	if (live_check && old_cluster.port == new_cluster.port)
+	if (user_opts.live_check && old_cluster.port == new_cluster.port)
 		pg_fatal("When checking a live server, "
 				 "the old and new port numbers must be different.");
 }
@@ -1838,7 +1838,7 @@ check_new_cluster_subscription_configuration(void)
  * before shutdown.
  */
 static void
-check_old_cluster_for_valid_slots(bool live_check)
+check_old_cluster_for_valid_slots(void)
 {
 	char		output_path[MAXPGPATH];
 	FILE	   *script = NULL;
@@ -1877,7 +1877,7 @@ check_old_cluster_for_valid_slots(bool live_check)
 			 * Note: This can be satisfied only when the old cluster has been
 			 * shut down, so we skip this for live checks.
 			 */
-			if (!live_check && !slot->caught_up)
+			if (!user_opts.live_check && !slot->caught_up)
 			{
 				if (script == NULL &&
 					(script = fopen_priv(output_path, "w")) == NULL)
diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c
index 1f0ccea3ed..854c6887a2 100644
--- a/src/bin/pg_upgrade/controldata.c
+++ b/src/bin/pg_upgrade/controldata.c
@@ -33,7 +33,7 @@
  * return valid xid data for a running server.
  */
 void
-get_control_data(ClusterInfo *cluster, bool live_check)
+get_control_data(ClusterInfo *cluster)
 {
 	char		cmd[MAXPGPATH];
 	char		bufin[MAX_STRING];
@@ -76,6 +76,7 @@ get_control_data(ClusterInfo *cluster, bool live_check)
 	uint32		segno = 0;
 	char	   *resetwal_bin;
 	int			rc;
+	bool		live_check = (cluster == &old_cluster && user_opts.live_check);
 
 	/*
 	 * Because we test the pg_resetwal output as strings, it has to be in
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index c07a69b63e..5de5e10945 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -27,7 +27,7 @@ static void free_rel_infos(RelInfoArr *rel_arr);
 static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
-static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check);
+static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo);
 
 
 /*
@@ -272,11 +272,9 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
  *
  * higher level routine to generate dbinfos for the database running
  * on the given "port". Assumes that server is already running.
- *
- * live_check would be used only when the target is the old cluster.
  */
 void
-get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check)
+get_db_rel_and_slot_infos(ClusterInfo *cluster)
 {
 	int			dbnum;
 
@@ -293,7 +291,7 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check)
 		get_rel_infos(cluster, pDbInfo);
 
 		if (cluster == &old_cluster)
-			get_old_cluster_logical_slot_infos(pDbInfo, live_check);
+			get_old_cluster_logical_slot_infos(pDbInfo);
 	}
 
 	if (cluster == &old_cluster)
@@ -637,7 +635,7 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
  * are included.
  */
 static void
-get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
+get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 {
 	PGconn	   *conn;
 	PGresult   *res;
@@ -673,7 +671,7 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo, bool live_check)
 							"WHERE slot_type = 'logical' AND "
 							"database = current_database() AND "
 							"temporary IS FALSE;",
-							live_check ? "FALSE" :
+							user_opts.live_check ? "FALSE" :
 							"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
 							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
 							"END)");
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index 548ea4e623..6f41d63eed 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -470,10 +470,10 @@ adjust_data_dir(ClusterInfo *cluster)
  * directory.
  */
 void
-get_sock_dir(ClusterInfo *cluster, bool live_check)
+get_sock_dir(ClusterInfo *cluster)
 {
 #if !defined(WIN32)
-	if (!live_check)
+	if (!user_opts.live_check || cluster == &new_cluster)
 		cluster->sockdir = user_opts.socketdir;
 	else
 	{
diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c
index 03eb738fd7..99f3d4543e 100644
--- a/src/bin/pg_upgrade/pg_upgrade.c
+++ b/src/bin/pg_upgrade/pg_upgrade.c
@@ -65,7 +65,7 @@ static void create_new_objects(void);
 static void copy_xact_xlog_xid(void);
 static void set_frozenxids(bool minmxid_only);
 static void make_outputdirs(char *pgdata);
-static void setup(char *argv0, bool *live_check);
+static void setup(char *argv0);
 static void create_logical_replication_slots(void);
 
 ClusterInfo old_cluster,
@@ -88,7 +88,6 @@ int
 main(int argc, char **argv)
 {
 	char	   *deletion_script_file_name = NULL;
-	bool		live_check = false;
 
 	/*
 	 * pg_upgrade doesn't currently use common/logging.c, but initialize it
@@ -123,18 +122,18 @@ main(int argc, char **argv)
 	 */
 	make_outputdirs(new_cluster.pgdata);
 
-	setup(argv[0], &live_check);
+	setup(argv[0]);
 
-	output_check_banner(live_check);
+	output_check_banner();
 
 	check_cluster_versions();
 
-	get_sock_dir(&old_cluster, live_check);
-	get_sock_dir(&new_cluster, false);
+	get_sock_dir(&old_cluster);
+	get_sock_dir(&new_cluster);
 
-	check_cluster_compatibility(live_check);
+	check_cluster_compatibility();
 
-	check_and_dump_old_cluster(live_check);
+	check_and_dump_old_cluster();
 
 
 	/* -- NEW -- */
@@ -331,7 +330,7 @@ make_outputdirs(char *pgdata)
 
 
 static void
-setup(char *argv0, bool *live_check)
+setup(char *argv0)
 {
 	/*
 	 * make sure the user has a clean environment, otherwise, we may confuse
@@ -378,7 +377,7 @@ setup(char *argv0, bool *live_check)
 				pg_fatal("There seems to be a postmaster servicing the old cluster.\n"
 						 "Please shutdown that postmaster and try again.");
 			else
-				*live_check = true;
+				user_opts.live_check = true;
 		}
 	}
 
@@ -660,7 +659,7 @@ create_new_objects(void)
 		set_frozenxids(true);
 
 	/* update new_cluster info now that we have objects in the databases */
-	get_db_rel_and_slot_infos(&new_cluster, false);
+	get_db_rel_and_slot_infos(&new_cluster);
 }
 
 /*
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index e2b99b49fa..2ef76ca2e7 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -322,6 +322,7 @@ typedef struct
 typedef struct
 {
 	bool		check;			/* check clusters only, don't change any data */
+	bool		live_check;		/* check clusters only, old server is running */
 	bool		do_sync;		/* flush changes to disk */
 	transferMode transfer_mode; /* copy files or link them? */
 	int			jobs;			/* number of processes/threads to use */
@@ -366,20 +367,20 @@ extern OSInfo os_info;
 
 /* check.c */
 
-void		output_check_banner(bool live_check);
-void		check_and_dump_old_cluster(bool live_check);
+void		output_check_banner(void);
+void		check_and_dump_old_cluster(void);
 void		check_new_cluster(void);
 void		report_clusters_compatible(void);
 void		issue_warnings_and_set_wal_level(void);
 void		output_completion_banner(char *deletion_script_file_name);
 void		check_cluster_versions(void);
-void		check_cluster_compatibility(bool live_check);
+void		check_cluster_compatibility(void);
 void		create_script_for_old_cluster_deletion(char **deletion_script_file_name);
 
 
 /* controldata.c */
 
-void		get_control_data(ClusterInfo *cluster, bool live_check);
+void		get_control_data(ClusterInfo *cluster);
 void		check_control_data(ControlData *oldctrl, ControlData *newctrl);
 void		disable_old_cluster(void);
 
@@ -428,7 +429,7 @@ void		check_loadable_libraries(void);
 FileNameMap *gen_db_file_maps(DbInfo *old_db,
 							  DbInfo *new_db, int *nmaps, const char *old_pgdata,
 							  const char *new_pgdata);
-void		get_db_rel_and_slot_infos(ClusterInfo *cluster, bool live_check);
+void		get_db_rel_and_slot_infos(ClusterInfo *cluster);
 int			count_old_cluster_logical_slots(void);
 void		get_subscription_count(ClusterInfo *cluster);
 
@@ -436,7 +437,7 @@ void		get_subscription_count(ClusterInfo *cluster);
 
 void		parseCommandLine(int argc, char *argv[]);
 void		adjust_data_dir(ClusterInfo *cluster);
-void		get_sock_dir(ClusterInfo *cluster, bool live_check);
+void		get_sock_dir(ClusterInfo *cluster);
 
 /* relfilenumber.c */
 
-- 
2.39.3 (Apple Git-146)

#15Daniel Gustafsson
daniel@yesql.se
In reply to: Nathan Bossart (#14)
Re: optimizing pg_upgrade's once-in-each-database steps

On 25 Jul 2024, at 04:58, Nathan Bossart <nathandbossart@gmail.com> wrote:

On Mon, Jul 22, 2024 at 03:07:10PM -0500, Nathan Bossart wrote:

Here is a new patch set. I've included the latest revision of the patch to
fix get_db_subscription_count() from the other thread [0] as 0001 since I
expect that to be committed soon. I've also moved the patch that moves the
"live_check" variable to "user_opts" to 0002 since I plan on committing
that sooner than later, too. Otherwise, I've tried to address all feedback
provided thus far.

Here is just the live_check patch. I rebased it, gave it a commit message,
and fixed a silly mistake. Barring objections or cfbot indigestion, I plan
to commit this within the next couple of days.

LGTM

--
Daniel Gustafsson

#16Nathan Bossart
nathandbossart@gmail.com
In reply to: Daniel Gustafsson (#15)
Re: optimizing pg_upgrade's once-in-each-database steps

On Thu, Jul 25, 2024 at 11:42:55AM +0200, Daniel Gustafsson wrote:

On 25 Jul 2024, at 04:58, Nathan Bossart <nathandbossart@gmail.com> wrote:
Here is just the live_check patch. I rebased it, gave it a commit message,
and fixed a silly mistake. Barring objections or cfbot indigestion, I plan
to commit this within the next couple of days.

LGTM

Thanks, committed.

--
nathan

#17Ilya Gladyshev
ilya.v.gladyshev@gmail.com
In reply to: Nathan Bossart (#13)
Re: optimizing pg_upgrade's once-in-each-database steps

On 22.07.2024 21:07, Nathan Bossart wrote:

On Fri, Jul 19, 2024 at 04:21:37PM -0500, Nathan Bossart wrote:

However, while looking into this, I noticed that only one get_query
callback (get_db_subscription_count()) actually customizes the generated
query using information in the provided DbInfo. AFAICT we can do this
particular step without running a query in each database, as I mentioned
elsewhere [0]. That should speed things up a bit and allow us to simplify
the AsyncTask code.

With that, if we are willing to assume that a given get_query callback will
generate the same string for all databases (and I think we should), we can
run the callback once and save the string in the step for dispatch_query()
to use. This would look more like what you suggested in the quoted text.

Here is a new patch set. I've included the latest revision of the patch to
fix get_db_subscription_count() from the other thread [0] as 0001 since I
expect that to be committed soon. I've also moved the patch that moves the
"live_check" variable to "user_opts" to 0002 since I plan on committing
that sooner than later, too. Otherwise, I've tried to address all feedback
provided thus far.

[0] https://commitfest.postgresql.org/49/5135/

Hi,

I like your idea of parallelizing these checks with async libpq API,
thanks for working on it. The patch doesn't apply cleanly on master
anymore, but I've rebased locally and taken it for a quick spin with a
pg16 instance of 1000 empty databases. Didn't see any regressions with
-j 1, there's some speedup with -j 8 (33 sec vs 8 sec for these checks).

One thing that I noticed that could be improved is we could start a new
connection right away after having run all query callbacks for the
current connection in process_slot, instead of just returning and
establishing the new connection only on the next iteration of the loop
in async_task_run after potentially sleeping on select.

+1 to Jeff's suggestion that perhaps we could reuse connections, but
perhaps that's a separate story.

Regards,

Ilya

#18Nathan Bossart
nathandbossart@gmail.com
In reply to: Ilya Gladyshev (#17)
Re: optimizing pg_upgrade's once-in-each-database steps

On Wed, Jul 31, 2024 at 10:55:33PM +0100, Ilya Gladyshev wrote:

I like your idea of parallelizing these checks with async libpq API, thanks
for working on it. The patch doesn't apply cleanly on master anymore, but
I've rebased locally and taken it for a quick spin with a pg16 instance of
1000 empty databases. Didn't see any regressions with -j 1, there's some
speedup with -j 8 (33 sec vs 8 sec for these checks).

Thanks for taking a look. I'm hoping to do a round of polishing before
posting a rebased patch set soon.

One thing that I noticed that could be improved is we could start a new
connection right away after having run all query callbacks for the current
connection in process_slot, instead of just returning and establishing the
new connection only on the next iteration of the loop in async_task_run
after potentially sleeping on select.

Yeah, we could just recursively call process_slot() right after freeing the
slot. That'd at least allow us to avoid the spinning behavior as we run
out of databases to process, if nothing else.

+1 to Jeff's suggestion that perhaps we could reuse connections, but perhaps
that's a separate story.

When I skimmed through the various tasks, I didn't see a ton of
opportunities for further consolidation, or at least opportunities that
would help for upgrades from supported versions. The data type checks are
already consolidated, for example.

--
nathan

#19Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#18)
11 attachment(s)
Re: optimizing pg_upgrade's once-in-each-database steps

On Thu, Aug 01, 2024 at 12:44:35PM -0500, Nathan Bossart wrote:

On Wed, Jul 31, 2024 at 10:55:33PM +0100, Ilya Gladyshev wrote:

I like your idea of parallelizing these checks with async libpq API, thanks
for working on it. The patch doesn't apply cleanly on master anymore, but
I've rebased locally and taken it for a quick spin with a pg16 instance of
1000 empty databases. Didn't see any regressions with -j 1, there's some
speedup with -j 8 (33 sec vs 8 sec for these checks).

Thanks for taking a look. I'm hoping to do a round of polishing before
posting a rebased patch set soon.

One thing that I noticed that could be improved is we could start a new
connection right away after having run all query callbacks for the current
connection in process_slot, instead of just returning and establishing the
new connection only on the next iteration of the loop in async_task_run
after potentially sleeping on select.

Yeah, we could just recursively call process_slot() right after freeing the
slot. That'd at least allow us to avoid the spinning behavior as we run
out of databases to process, if nothing else.

Here is a new patch set. Besides rebasing, I've added the recursive call
to process_slot() mentioned in the quoted text, and I've added quite a bit
of commentary to async.c.

--
nathan

Attachments:

v7-0001-introduce-framework-for-parallelizing-pg_upgrade-.patchtext/plain; charset=us-asciiDownload
From 0331c1bbb8bee8fa079c761c4a0174212312e709 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 11:02:44 -0500
Subject: [PATCH v7 01/11] introduce framework for parallelizing pg_upgrade
 tasks

---
 src/bin/pg_upgrade/Makefile      |   1 +
 src/bin/pg_upgrade/async.c       | 505 +++++++++++++++++++++++++++++++
 src/bin/pg_upgrade/meson.build   |   1 +
 src/bin/pg_upgrade/pg_upgrade.h  |  16 +
 src/tools/pgindent/typedefs.list |   4 +
 5 files changed, 527 insertions(+)
 create mode 100644 src/bin/pg_upgrade/async.c

diff --git a/src/bin/pg_upgrade/Makefile b/src/bin/pg_upgrade/Makefile
index bde91e2beb..3bc4f5d740 100644
--- a/src/bin/pg_upgrade/Makefile
+++ b/src/bin/pg_upgrade/Makefile
@@ -12,6 +12,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	$(WIN32RES) \
+	async.o \
 	check.o \
 	controldata.o \
 	dump.o \
diff --git a/src/bin/pg_upgrade/async.c b/src/bin/pg_upgrade/async.c
new file mode 100644
index 0000000000..b5b36c4725
--- /dev/null
+++ b/src/bin/pg_upgrade/async.c
@@ -0,0 +1,505 @@
+/*
+ * async.c
+ *		parallelization via libpq's async APIs
+ *
+ * This framework provides an efficient way of running the various
+ * once-in-each-database tasks required by pg_upgrade.  Specifically, it
+ * parallelizes these tasks by managing a simple state machine of
+ * user_opts.jobs slots and using libpq's asynchronous APIs to establish the
+ * connections and run the queries.  Callers simply need to create a couple of
+ * callback functions and build/execute an AsyncTask.  A simple example
+ * follows:
+ *
+ *		static char *
+ *		my_query_cb(void *arg)
+ *		{
+ *			// NB: RESULT MUST BE ALLOC'D!
+ *			return pg_strdup("... query text ...");
+ *		}
+ *
+ *		static void
+ *		my_process_cb(DbInfo *dbinfo, PGresult *res, void *arg)
+ *		{
+ *			for (int i = 0; i < PQntuples(res); i++)
+ *			{
+ *				... process results ...
+ *			}
+ *		}
+ *
+ *		void
+ *		my_task(ClusterInfo *cluster)
+ *		{
+ *			AsyncTask  *task = async_task_create();
+ *
+ *			async_task_add_step(task,
+ *								my_query_cb,
+ *								my_process_cb,
+ *								true,	// let the task free the PGresult
+ *								NULL);	// "arg" pointer for the callbacks
+ *			async_task_run(task, cluster);
+ *			async_task_free(task);
+ *		}
+ *
+ * Note that multiple steps can be added to a given task.  When there are
+ * multiple steps, the task will run all of the steps consecutively in the same
+ * database connection before freeing the connection and moving on.  In other
+ * words, it only ever initiates one connection to each database in the
+ * cluster for a given run.
+ *
+ * Also note that the query callbacks (i.e., the AsyncTaskGetQueryCB given to
+ * the task) are only run once, and their result is stored and reused in all
+ * databases.  In practice, this means that the query callbacks cannot depend
+ * on anything database-specific.  If you find yourself trying to use
+ * database-specific queries, chances are this is the wrong tool for the job.
+ *
+ * As shown in the example above, the query callbacks must return an alloc'd
+ * query string.  This string will be freed by async_task_free().
+ *
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ * src/bin/pg_upgrade/async.c
+ */
+
+#include "postgres_fe.h"
+
+#include "common/connect.h"
+#include "fe_utils/string_utils.h"
+#include "pg_upgrade.h"
+
+/*
+ * dbs_complete stores the number of databases that we have completed
+ * processing.  When this value equals the number of databases in the cluster,
+ * the task is finished.
+ */
+static int	dbs_complete;
+
+/*
+ * dbs_processing stores the index of the next database in the cluster's array
+ * of databases that will be picked up for processing.  It will always be
+ * greater than or equal to dbs_complete.
+ */
+static int	dbs_processing;
+
+/*
+ * This struct stores all the information for a single step of a task.  All
+ * steps in a task are run in a single connection before moving on to the next
+ * database (which requires a new connection).  Note that the query_cb will
+ * only be executed once, and its result will be reused for all databases in
+ * the cluster.
+ */
+typedef struct AsyncTaskCallbacks
+{
+	AsyncTaskGetQueryCB query_cb;	/* returns an alloc'd query string */
+	AsyncTaskProcessCB process_cb;	/* processes the results of the query */
+	char	   *query;			/* stores the result of query_cb */
+	bool		free_result;	/* should we free the result? */
+	void	   *arg;			/* pointer passed to each callback */
+} AsyncTaskCallbacks;
+
+/*
+ * This struct is a thin wrapper around an array of steps, i.e.,
+ * AsyncTaskCallbacks.
+ */
+typedef struct AsyncTask
+{
+	AsyncTaskCallbacks *cbs;
+	int			num_cb_sets;
+} AsyncTask;
+
+/*
+ * The different states for a parallel slot.
+ */
+typedef enum
+{
+	FREE,						/* slot available for use in a new database */
+	CONNECTING,					/* waiting for connection to be established */
+	SETTING_SEARCH_PATH,		/* waiting for search_path query to complete */
+	RUNNING_QUERY,				/* running/processing queries in the task */
+} AsyncSlotState;
+
+/*
+ * We maintain an array of user_opts.jobs slots to execute the task.
+ */
+typedef struct
+{
+	AsyncSlotState state;		/* state of the slot */
+	int			db;				/* index of the database assigned to slot */
+	int			query;			/* index of the current step in the task */
+	PGconn	   *conn;			/* current connection managed by slot */
+} AsyncSlot;
+
+/*
+ * Initializes an AsyncTask.
+ */
+AsyncTask *
+async_task_create(void)
+{
+	return pg_malloc0(sizeof(AsyncTask));
+}
+
+/*
+ * Frees all storage associated with an AsyncTask.
+ */
+void
+async_task_free(AsyncTask *task)
+{
+	for (int i = 0; i < task->num_cb_sets; i++)
+	{
+		if (task->cbs[i].query)
+			pg_free(task->cbs[i].query);
+	}
+
+	if (task->cbs)
+		pg_free(task->cbs);
+
+	pg_free(task);
+}
+
+/*
+ * Adds a step to an AsyncTask.  The steps will be executed in each database in
+ * the order in which they are added.
+ *
+ *	task: task object that must have been initialized via async_task_create()
+ *	query_cb: function that returns an alloc'd query string
+ *	process_cb: function that processes the results of the query
+ *	free_result: should we free the PGresult, or leave it to the caller?
+ *	arg: pointer to task-specific data that is passed to each callback
+ */
+void
+async_task_add_step(AsyncTask *task,
+					AsyncTaskGetQueryCB query_cb,
+					AsyncTaskProcessCB process_cb, bool free_result,
+					void *arg)
+{
+	AsyncTaskCallbacks *new_cbs;
+
+	task->cbs = pg_realloc(task->cbs,
+						   ++task->num_cb_sets * sizeof(AsyncTaskCallbacks));
+
+	new_cbs = &task->cbs[task->num_cb_sets - 1];
+	new_cbs->query_cb = query_cb;
+	new_cbs->process_cb = process_cb;
+	new_cbs->query = NULL;
+	new_cbs->free_result = free_result;
+	new_cbs->arg = arg;
+}
+
+/*
+ * A simple wrapper around a pg_fatal() that includes the error message for the
+ * connection.
+ */
+static void
+conn_failure(PGconn *conn)
+{
+	pg_fatal("connection failure: %s", PQerrorMessage(conn));
+}
+
+/*
+ * Build a connection string for the slot's current database and asynchronously
+ * start a new connection, but do not wait for the connection to be
+ * established.
+ */
+static void
+start_conn(const ClusterInfo *cluster, AsyncSlot *slot)
+{
+	PQExpBufferData conn_opts;
+
+	/* Build connection string with proper quoting */
+	initPQExpBuffer(&conn_opts);
+	appendPQExpBufferStr(&conn_opts, "dbname=");
+	appendConnStrVal(&conn_opts, cluster->dbarr.dbs[slot->db].db_name);
+	appendPQExpBufferStr(&conn_opts, " user=");
+	appendConnStrVal(&conn_opts, os_info.user);
+	appendPQExpBuffer(&conn_opts, " port=%d", cluster->port);
+	if (cluster->sockdir)
+	{
+		appendPQExpBufferStr(&conn_opts, " host=");
+		appendConnStrVal(&conn_opts, cluster->sockdir);
+	}
+
+	slot->conn = PQconnectStart(conn_opts.data);
+	termPQExpBuffer(&conn_opts);
+
+	if (!slot->conn)
+		conn_failure(slot->conn);
+}
+
+/*
+ * Start the next query, but do not wait for it to complete.
+ */
+static void
+dispatch_query(const ClusterInfo *cluster, AsyncSlot *slot,
+			   const AsyncTask *task)
+{
+	AsyncTaskCallbacks *cbs = &task->cbs[slot->query];
+
+	/*
+	 * Note that we store the result of the query callback and reuse it for
+	 * all databases in the cluster, so there cannot be anything
+	 * database-specific in the query string.  If your query needs something
+	 * database-specific, chances are that you shouldn't be using the
+	 * AsyncTask functionality.
+	 */
+	if (!cbs->query)
+		cbs->query = cbs->query_cb(cbs->arg);
+
+	if (!PQsendQuery(slot->conn, cbs->query))
+		conn_failure(slot->conn);
+}
+
+/*
+ * Cycle through all the results for a connection and return the last one.  We
+ * don't anticipate there ever being more than a single result for anything we
+ * do, so this is mostly pro forma.
+ */
+static PGresult *
+get_last_result(PGconn *conn)
+{
+	PGresult   *tmp;
+	PGresult   *res = NULL;
+
+	while ((tmp = PQgetResult(conn)) != NULL)
+	{
+		PQclear(res);
+		res = tmp;
+		if (PQstatus(conn) == CONNECTION_BAD)
+			conn_failure(conn);
+	}
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK &&
+		PQresultStatus(res) != PGRES_TUPLES_OK)
+		conn_failure(conn);
+
+	return res;
+}
+
+/*
+ * Run the process_cb callback function to process the result of a query, and
+ * free the result if the caller indicated we should do so.
+ */
+static void
+process_query_result(const ClusterInfo *cluster, AsyncSlot *slot,
+					 const AsyncTask *task)
+{
+	AsyncTaskCallbacks *cbs = &task->cbs[slot->query];
+	AsyncTaskProcessCB process_cb = cbs->process_cb;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db];
+	PGresult   *res = get_last_result(slot->conn);
+
+	(*process_cb) (dbinfo, res, cbs->arg);
+
+	if (cbs->free_result)
+		PQclear(res);
+}
+
+/*
+ * Advances the state machine for a given slot as necessary.
+ */
+static void
+process_slot(const ClusterInfo *cluster, AsyncSlot *slot, const AsyncTask *task)
+{
+	switch (slot->state)
+	{
+		case FREE:
+
+			/*
+			 * If all of the databases in the cluster have been processed or
+			 * are currently being processed by other slots, we are done.
+			 */
+			if (dbs_processing >= cluster->dbarr.ndbs)
+				return;
+
+			/*
+			 * Claim the next database in the cluster's array and initiate a
+			 * new connection.
+			 */
+			slot->db = dbs_processing++;
+			slot->state = CONNECTING;
+			start_conn(cluster, slot);
+
+			return;
+
+		case CONNECTING:
+
+			/* Check for connection failure. */
+			if (PQconnectPoll(slot->conn) == PGRES_POLLING_FAILED)
+				conn_failure(slot->conn);
+
+			/* Check whether the connection is still establishing. */
+			if (PQconnectPoll(slot->conn) != PGRES_POLLING_OK)
+				return;
+
+			/*
+			 * Move on to setting the search_path for the connection to a
+			 * known-safe value.  This is common for all tasks/steps and
+			 * should always be done first.
+			 */
+			slot->state = SETTING_SEARCH_PATH;
+			if (!PQsendQuery(slot->conn, ALWAYS_SECURE_SEARCH_PATH_SQL))
+				conn_failure(slot->conn);
+
+			return;
+
+		case SETTING_SEARCH_PATH:
+
+			/* Check whether the query is still in progress. */
+			if (!PQconsumeInput(slot->conn))
+				conn_failure(slot->conn);
+			if (PQisBusy(slot->conn))
+				return;
+
+			/* Discard the result of the search_path query. */
+			PQclear(get_last_result(slot->conn));
+
+			/* Start running the query for the first step in the task. */
+			slot->state = RUNNING_QUERY;
+			dispatch_query(cluster, slot, task);
+
+			return;
+
+		case RUNNING_QUERY:
+
+			/* Check whether the query is still in progress. */
+			if (!PQconsumeInput(slot->conn))
+				conn_failure(slot->conn);
+			if (PQisBusy(slot->conn))
+				return;
+
+			/* Process the query result. */
+			process_query_result(cluster, slot, task);
+
+			/*
+			 * If we just finished processing the result of the last step in
+			 * the task, free the slot.  We recursively call this function on
+			 * the newly-freed slot so that we can start initiating the next
+			 * connection immediately instead of waiting for the next loop
+			 * through the slots.
+			 */
+			if (++slot->query >= task->num_cb_sets)
+			{
+				dbs_complete++;
+				PQfinish(slot->conn);
+				memset(slot, 0, sizeof(AsyncSlot));
+
+				process_slot(cluster, slot, task);
+
+				return;
+			}
+
+			/* Start running the query for the next step in the task. */
+			dispatch_query(cluster, slot, task);
+			return;
+	}
+}
+
+/*
+ * Wait on the slots to either finish connecting or to receive query results if
+ * possible.  This avoids a tight loop in async_task_run().
+ */
+static void
+wait_on_slots(AsyncSlot *slots, int numslots)
+{
+	fd_set		input_mask;
+	fd_set		output_mask;
+	fd_set		except_mask;
+	int			maxFd = 0;
+
+	FD_ZERO(&input_mask);
+	FD_ZERO(&output_mask);
+	FD_ZERO(&except_mask);
+
+	for (int i = 0; i < numslots; i++)
+	{
+		int			sock;
+		bool		read = false;
+
+		switch (slots[i].state)
+		{
+			case FREE:
+
+				/*
+				 * This function should only ever see free slots as we are
+				 * finishing processing the last few databases, at which point
+				 * we don't have any databases left for them to process. We'll
+				 * never use these slots again, so we can safely ignore them.
+				 */
+				continue;
+
+			case CONNECTING:
+
+				/*
+				 * If we are waiting for the connection to establish, choose
+				 * whether to wait for reading or for writing on the socket as
+				 * appropriate.  If neither apply, just return immediately so
+				 * that we can handle the slot.
+				 */
+				{
+					PostgresPollingStatusType status;
+
+					status = PQconnectPoll(slots[i].conn);
+					if (status == PGRES_POLLING_READING)
+						read = true;
+					else if (status != PGRES_POLLING_WRITING)
+						return;
+				}
+				break;
+
+			case SETTING_SEARCH_PATH:
+			case RUNNING_QUERY:
+
+				/*
+				 * If we've sent a query, we must wait for the socket to be
+				 * read-ready.  Note that process_slot() handles calling
+				 * PQconsumeInput() as required.
+				 */
+				read = true;
+				break;
+		}
+
+		/*
+		 * If there's some problem retrieving the socket, just pretend this
+		 * slot doesn't exist.  We don't expect this to happen regularly in
+		 * practice, so it seems unlikely to cause too much harm.
+		 */
+		sock = PQsocket(slots[i].conn);
+		if (sock < 0)
+			continue;
+
+		/*
+		 * Add the socket to the set.
+		 */
+		FD_SET(sock, read ? &input_mask : &output_mask);
+		FD_SET(sock, &except_mask);
+		maxFd = Max(maxFd, sock);
+	}
+
+	/*
+	 * If we found socket(s) to wait on, wait.
+	 */
+	if (maxFd != 0)
+		(void) select(maxFd + 1, &input_mask, &output_mask, &except_mask, NULL);
+}
+
+/*
+ * Runs all the steps of the task in every database in the cluster using
+ * user_opts.jobs parallel slots.
+ */
+void
+async_task_run(const AsyncTask *task, const ClusterInfo *cluster)
+{
+	int			jobs = Max(1, user_opts.jobs);
+	AsyncSlot  *slots = pg_malloc0(sizeof(AsyncSlot) * jobs);
+
+	dbs_complete = 0;
+	dbs_processing = 0;
+
+	while (dbs_complete < cluster->dbarr.ndbs)
+	{
+		for (int i = 0; i < jobs; i++)
+			process_slot(cluster, &slots[i], task);
+
+		wait_on_slots(slots, jobs);
+	}
+
+	pg_free(slots);
+}
diff --git a/src/bin/pg_upgrade/meson.build b/src/bin/pg_upgrade/meson.build
index 9825fa3305..9eb48e176c 100644
--- a/src/bin/pg_upgrade/meson.build
+++ b/src/bin/pg_upgrade/meson.build
@@ -1,6 +1,7 @@
 # Copyright (c) 2022-2024, PostgreSQL Global Development Group
 
 pg_upgrade_sources = files(
+  'async.c',
   'check.c',
   'controldata.c',
   'dump.c',
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index cdb6e2b759..104ae75f9f 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -494,3 +494,19 @@ void		parallel_transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr
 										  char *old_pgdata, char *new_pgdata,
 										  char *old_tablespace);
 bool		reap_child(bool wait_for_child);
+
+/* async.c */
+
+typedef char *(*AsyncTaskGetQueryCB) (void *arg);
+typedef void (*AsyncTaskProcessCB) (DbInfo *dbinfo, PGresult *res, void *arg);
+
+/* struct definition is private to async.c */
+typedef struct AsyncTask AsyncTask;
+
+AsyncTask  *async_task_create(void);
+void		async_task_add_step(AsyncTask *task,
+								AsyncTaskGetQueryCB query_cb,
+								AsyncTaskProcessCB process_cb, bool free_result,
+								void *arg);
+void		async_task_run(const AsyncTask *task, const ClusterInfo *cluster);
+void		async_task_free(AsyncTask *task);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8de9978ad8..2c43a7adcc 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -153,6 +153,10 @@ ArrayMetaState
 ArraySubWorkspace
 ArrayToken
 ArrayType
+AsyncSlot
+AsyncSlotState
+AsyncTask
+AsyncTaskCallbacks
 AsyncQueueControl
 AsyncQueueEntry
 AsyncRequest
-- 
2.39.3 (Apple Git-146)

v7-0002-use-new-pg_upgrade-async-API-for-subscription-sta.patchtext/plain; charset=us-asciiDownload
From 404d29d4c01af20d0efb4f0f5c2e92e2a9329370 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 17:21:19 -0500
Subject: [PATCH v7 02/11] use new pg_upgrade async API for subscription state
 checks

---
 src/bin/pg_upgrade/check.c | 204 ++++++++++++++++++++-----------------
 1 file changed, 110 insertions(+), 94 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 5038231731..44b8f16df9 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1905,6 +1905,79 @@ check_old_cluster_for_valid_slots(void)
 	check_ok();
 }
 
+/* private state for subscription state checks */
+struct substate_info
+{
+	FILE	   *script;
+	char		output_path[MAXPGPATH];
+};
+
+/*
+ * We don't allow upgrade if there is a risk of dangling slot or origin
+ * corresponding to initial sync after upgrade.
+ *
+ * A slot/origin not created yet refers to the 'i' (initialize) state, while
+ * 'r' (ready) state refers to a slot/origin created previously but already
+ * dropped. These states are supported for pg_upgrade. The other states listed
+ * below are not supported:
+ *
+ * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state would
+ * retain a replication slot, which could not be dropped by the sync worker
+ * spawned after the upgrade because the subscription ID used for the slot name
+ * won't match anymore.
+ *
+ * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state would
+ * retain the replication origin when there is a failure in tablesync worker
+ * immediately after dropping the replication slot in the publisher.
+ *
+ * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on a
+ * relation upgraded while in this state would expect an origin ID with the OID
+ * of the subscription used before the upgrade, causing it to fail.
+ *
+ * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and SUBREL_STATE_UNKNOWN:
+ * These states are not stored in the catalog, so we need not allow these
+ * states.
+ */
+static char *
+sub_query(void *arg)
+{
+	return pg_strdup("SELECT r.srsubstate, s.subname, n.nspname, c.relname "
+					 "FROM pg_catalog.pg_subscription_rel r "
+					 "LEFT JOIN pg_catalog.pg_subscription s"
+					 "   ON r.srsubid = s.oid "
+					 "LEFT JOIN pg_catalog.pg_class c"
+					 "   ON r.srrelid = c.oid "
+					 "LEFT JOIN pg_catalog.pg_namespace n"
+					 "   ON c.relnamespace = n.oid "
+					 "WHERE r.srsubstate NOT IN ('i', 'r') "
+					 "ORDER BY s.subname");
+}
+
+static void
+sub_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct substate_info *state = (struct substate_info *) arg;
+	int			ntup = PQntuples(res);
+	int			i_srsubstate = PQfnumber(res, "srsubstate");
+	int			i_subname = PQfnumber(res, "subname");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+
+	for (int i = 0; i < ntup; i++)
+	{
+		if (state->script == NULL &&
+			(state->script = fopen_priv(state->output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state->output_path);
+
+		fprintf(state->script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
+				PQgetvalue(res, i, i_srsubstate),
+				dbinfo->db_name,
+				PQgetvalue(res, i, i_subname),
+				PQgetvalue(res, i, i_nspname),
+				PQgetvalue(res, i, i_relname));
+	}
+}
+
 /*
  * check_old_cluster_subscription_state()
  *
@@ -1915,115 +1988,58 @@ check_old_cluster_for_valid_slots(void)
 static void
 check_old_cluster_subscription_state(void)
 {
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
+	struct substate_info state;
+	PGresult   *res;
+	PGconn	   *conn;
 	int			ntup;
 
 	prep_status("Checking for subscription state");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	state.script = NULL;
+	snprintf(state.output_path, sizeof(state.output_path), "%s/%s",
 			 log_opts.basedir,
 			 "subs_invalid.txt");
-	for (int dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
-
-		/* We need to check for pg_replication_origin only once. */
-		if (dbnum == 0)
-		{
-			/*
-			 * Check that all the subscriptions have their respective
-			 * replication origin.
-			 */
-			res = executeQueryOrDie(conn,
-									"SELECT d.datname, s.subname "
-									"FROM pg_catalog.pg_subscription s "
-									"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
-									"	ON o.roname = 'pg_' || s.oid "
-									"INNER JOIN pg_catalog.pg_database d "
-									"	ON d.oid = s.subdbid "
-									"WHERE o.roname IS NULL;");
-
-			ntup = PQntuples(res);
-			for (int i = 0; i < ntup; i++)
-			{
-				if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-					pg_fatal("could not open file \"%s\": %m", output_path);
-				fprintf(script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
-						PQgetvalue(res, i, 0),
-						PQgetvalue(res, i, 1));
-			}
-			PQclear(res);
-		}
 
-		/*
-		 * We don't allow upgrade if there is a risk of dangling slot or
-		 * origin corresponding to initial sync after upgrade.
-		 *
-		 * A slot/origin not created yet refers to the 'i' (initialize) state,
-		 * while 'r' (ready) state refers to a slot/origin created previously
-		 * but already dropped. These states are supported for pg_upgrade. The
-		 * other states listed below are not supported:
-		 *
-		 * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state
-		 * would retain a replication slot, which could not be dropped by the
-		 * sync worker spawned after the upgrade because the subscription ID
-		 * used for the slot name won't match anymore.
-		 *
-		 * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state
-		 * would retain the replication origin when there is a failure in
-		 * tablesync worker immediately after dropping the replication slot in
-		 * the publisher.
-		 *
-		 * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on
-		 * a relation upgraded while in this state would expect an origin ID
-		 * with the OID of the subscription used before the upgrade, causing
-		 * it to fail.
-		 *
-		 * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and
-		 * SUBREL_STATE_UNKNOWN: These states are not stored in the catalog,
-		 * so we need not allow these states.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT r.srsubstate, s.subname, n.nspname, c.relname "
-								"FROM pg_catalog.pg_subscription_rel r "
-								"LEFT JOIN pg_catalog.pg_subscription s"
-								"	ON r.srsubid = s.oid "
-								"LEFT JOIN pg_catalog.pg_class c"
-								"	ON r.srrelid = c.oid "
-								"LEFT JOIN pg_catalog.pg_namespace n"
-								"	ON c.relnamespace = n.oid "
-								"WHERE r.srsubstate NOT IN ('i', 'r') "
-								"ORDER BY s.subname");
-
-		ntup = PQntuples(res);
-		for (int i = 0; i < ntup; i++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
+	/*
+	 * Check that all the subscriptions have their respective replication
+	 * origin.  This check only needs to run once.
+	 */
+	conn = connectToServer(&old_cluster, old_cluster.dbarr.dbs[0].db_name);
+	res = executeQueryOrDie(conn,
+							"SELECT d.datname, s.subname "
+							"FROM pg_catalog.pg_subscription s "
+							"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
+							"	ON o.roname = 'pg_' || s.oid "
+							"INNER JOIN pg_catalog.pg_database d "
+							"	ON d.oid = s.subdbid "
+							"WHERE o.roname IS NULL;");
+	ntup = PQntuples(res);
+	for (int i = 0; i < ntup; i++)
+	{
+		if (state.script == NULL &&
+			(state.script = fopen_priv(state.output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state.output_path);
+		fprintf(state.script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
+				PQgetvalue(res, i, 0),
+				PQgetvalue(res, i, 1));
+	}
+	PQclear(res);
+	PQfinish(conn);
 
-			fprintf(script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
-					PQgetvalue(res, i, 0),
-					active_db->db_name,
-					PQgetvalue(res, i, 1),
-					PQgetvalue(res, i, 2),
-					PQgetvalue(res, i, 3));
-		}
+	async_task_add_step(task, sub_query, sub_process, true, &state);
 
-		PQclear(res);
-		PQfinish(conn);
-	}
+	async_task_run(task, &old_cluster);
+	async_task_free(task);
 
-	if (script)
+	if (state.script)
 	{
-		fclose(script);
+		fclose(state.script);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains subscriptions without origin or having relations not in i (initialize) or r (ready) state.\n"
 				 "You can allow the initial sync to finish for all relations and then restart the upgrade.\n"
 				 "A list of the problematic subscriptions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", state.output_path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v7-0003-use-new-pg_upgrade-async-API-for-retrieving-relin.patchtext/plain; charset=us-asciiDownload
From 100d413e46c67d00c35b3dccc1ee28098a6c3f08 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:09:33 -0500
Subject: [PATCH v7 03/11] use new pg_upgrade async API for retrieving relinfos

---
 src/bin/pg_upgrade/info.c | 238 ++++++++++++++++++--------------------
 1 file changed, 110 insertions(+), 128 deletions(-)

diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 5de5e10945..30a6d30284 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -11,6 +11,7 @@
 
 #include "access/transam.h"
 #include "catalog/pg_class_d.h"
+#include "pqexpbuffer.h"
 #include "pg_upgrade.h"
 
 static void create_rel_filename_map(const char *old_data, const char *new_data,
@@ -22,12 +23,14 @@ static void report_unmatched_relation(const RelInfo *rel, const DbInfo *db,
 static void free_db_and_rel_infos(DbInfoArr *db_arr);
 static void get_template0_info(ClusterInfo *cluster);
 static void get_db_infos(ClusterInfo *cluster);
-static void get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo);
+static char *get_rel_infos_query(void *arg);
+static void get_rel_infos_result(DbInfo *dbinfo, PGresult *res, void *arg);
 static void free_rel_infos(RelInfoArr *rel_arr);
 static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
-static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo);
+static char *get_old_cluster_logical_slot_infos_query(void *arg);
+static void get_old_cluster_logical_slot_infos_result(DbInfo *dbinfo, PGresult *res, void *arg);
 
 
 /*
@@ -276,7 +279,7 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
 void
 get_db_rel_and_slot_infos(ClusterInfo *cluster)
 {
-	int			dbnum;
+	AsyncTask  *task = async_task_create();
 
 	if (cluster->dbarr.dbs != NULL)
 		free_db_and_rel_infos(&cluster->dbarr);
@@ -284,15 +287,19 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster)
 	get_template0_info(cluster);
 	get_db_infos(cluster);
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		DbInfo	   *pDbInfo = &cluster->dbarr.dbs[dbnum];
-
-		get_rel_infos(cluster, pDbInfo);
+	async_task_add_step(task,
+						get_rel_infos_query,
+						get_rel_infos_result,
+						true, NULL);
+	if (cluster == &old_cluster &&
+		GET_MAJOR_VERSION(cluster->major_version) > 1600)
+		async_task_add_step(task,
+							get_old_cluster_logical_slot_infos_query,
+							get_old_cluster_logical_slot_infos_result,
+							true, NULL);
 
-		if (cluster == &old_cluster)
-			get_old_cluster_logical_slot_infos(pDbInfo);
-	}
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (cluster == &old_cluster)
 		pg_log(PG_VERBOSE, "\nsource databases:");
@@ -439,32 +446,12 @@ get_db_infos(ClusterInfo *cluster)
  * Note: the resulting RelInfo array is assumed to be sorted by OID.
  * This allows later processing to match up old and new databases efficiently.
  */
-static void
-get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
+static char *
+get_rel_infos_query(void *arg)
 {
-	PGconn	   *conn = connectToServer(cluster,
-									   dbinfo->db_name);
-	PGresult   *res;
-	RelInfo    *relinfos;
-	int			ntups;
-	int			relnum;
-	int			num_rels = 0;
-	char	   *nspname = NULL;
-	char	   *relname = NULL;
-	char	   *tablespace = NULL;
-	int			i_spclocation,
-				i_nspname,
-				i_relname,
-				i_reloid,
-				i_indtable,
-				i_toastheap,
-				i_relfilenumber,
-				i_reltablespace;
-	char		query[QUERY_ALLOC];
-	char	   *last_namespace = NULL,
-			   *last_tablespace = NULL;
+	PQExpBufferData query;
 
-	query[0] = '\0';			/* initialize query string to empty */
+	initPQExpBuffer(&query);
 
 	/*
 	 * Create a CTE that collects OIDs of regular user tables and matviews,
@@ -476,34 +463,34 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * output, so we have to copy that system table.  It's easiest to do that
 	 * by treating it as a user table.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "WITH regular_heap (reloid, indtable, toastheap) AS ( "
-			 "  SELECT c.oid, 0::oid, 0::oid "
-			 "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
-			 "         ON c.relnamespace = n.oid "
-			 "  WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
-			 CppAsString2(RELKIND_MATVIEW) ") AND "
+	appendPQExpBuffer(&query,
+					  "WITH regular_heap (reloid, indtable, toastheap) AS ( "
+					  "  SELECT c.oid, 0::oid, 0::oid "
+					  "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
+					  "         ON c.relnamespace = n.oid "
+					  "  WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
+					  CppAsString2(RELKIND_MATVIEW) ") AND "
 	/* exclude possible orphaned temp tables */
-			 "    ((n.nspname !~ '^pg_temp_' AND "
-			 "      n.nspname !~ '^pg_toast_temp_' AND "
-			 "      n.nspname NOT IN ('pg_catalog', 'information_schema', "
-			 "                        'binary_upgrade', 'pg_toast') AND "
-			 "      c.oid >= %u::pg_catalog.oid) OR "
-			 "     (n.nspname = 'pg_catalog' AND "
-			 "      relname IN ('pg_largeobject') ))), ",
-			 FirstNormalObjectId);
+					  "    ((n.nspname !~ '^pg_temp_' AND "
+					  "      n.nspname !~ '^pg_toast_temp_' AND "
+					  "      n.nspname NOT IN ('pg_catalog', 'information_schema', "
+					  "                        'binary_upgrade', 'pg_toast') AND "
+					  "      c.oid >= %u::pg_catalog.oid) OR "
+					  "     (n.nspname = 'pg_catalog' AND "
+					  "      relname IN ('pg_largeobject') ))), ",
+					  FirstNormalObjectId);
 
 	/*
 	 * Add a CTE that collects OIDs of toast tables belonging to the tables
 	 * selected by the regular_heap CTE.  (We have to do this separately
 	 * because the namespace-name rules above don't work for toast tables.)
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "  toast_heap (reloid, indtable, toastheap) AS ( "
-			 "  SELECT c.reltoastrelid, 0::oid, c.oid "
-			 "  FROM regular_heap JOIN pg_catalog.pg_class c "
-			 "      ON regular_heap.reloid = c.oid "
-			 "  WHERE c.reltoastrelid != 0), ");
+	appendPQExpBufferStr(&query,
+						 "  toast_heap (reloid, indtable, toastheap) AS ( "
+						 "  SELECT c.reltoastrelid, 0::oid, c.oid "
+						 "  FROM regular_heap JOIN pg_catalog.pg_class c "
+						 "      ON regular_heap.reloid = c.oid "
+						 "  WHERE c.reltoastrelid != 0), ");
 
 	/*
 	 * Add a CTE that collects OIDs of all valid indexes on the previously
@@ -511,53 +498,61 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * Testing indisready is necessary in 9.2, and harmless in earlier/later
 	 * versions.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "  all_index (reloid, indtable, toastheap) AS ( "
-			 "  SELECT indexrelid, indrelid, 0::oid "
-			 "  FROM pg_catalog.pg_index "
-			 "  WHERE indisvalid AND indisready "
-			 "    AND indrelid IN "
-			 "        (SELECT reloid FROM regular_heap "
-			 "         UNION ALL "
-			 "         SELECT reloid FROM toast_heap)) ");
+	appendPQExpBufferStr(&query,
+						 "  all_index (reloid, indtable, toastheap) AS ( "
+						 "  SELECT indexrelid, indrelid, 0::oid "
+						 "  FROM pg_catalog.pg_index "
+						 "  WHERE indisvalid AND indisready "
+						 "    AND indrelid IN "
+						 "        (SELECT reloid FROM regular_heap "
+						 "         UNION ALL "
+						 "         SELECT reloid FROM toast_heap)) ");
 
 	/*
 	 * And now we can write the query that retrieves the data we want for each
 	 * heap and index relation.  Make sure result is sorted by OID.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "SELECT all_rels.*, n.nspname, c.relname, "
-			 "  c.relfilenode, c.reltablespace, "
-			 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
-			 "FROM (SELECT * FROM regular_heap "
-			 "      UNION ALL "
-			 "      SELECT * FROM toast_heap "
-			 "      UNION ALL "
-			 "      SELECT * FROM all_index) all_rels "
-			 "  JOIN pg_catalog.pg_class c "
-			 "      ON all_rels.reloid = c.oid "
-			 "  JOIN pg_catalog.pg_namespace n "
-			 "     ON c.relnamespace = n.oid "
-			 "  LEFT OUTER JOIN pg_catalog.pg_tablespace t "
-			 "     ON c.reltablespace = t.oid "
-			 "ORDER BY 1;");
-
-	res = executeQueryOrDie(conn, "%s", query);
-
-	ntups = PQntuples(res);
-
-	relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	appendPQExpBufferStr(&query,
+						 "SELECT all_rels.*, n.nspname, c.relname, "
+						 "  c.relfilenode, c.reltablespace, "
+						 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
+						 "FROM (SELECT * FROM regular_heap "
+						 "      UNION ALL "
+						 "      SELECT * FROM toast_heap "
+						 "      UNION ALL "
+						 "      SELECT * FROM all_index) all_rels "
+						 "  JOIN pg_catalog.pg_class c "
+						 "      ON all_rels.reloid = c.oid "
+						 "  JOIN pg_catalog.pg_namespace n "
+						 "     ON c.relnamespace = n.oid "
+						 "  LEFT OUTER JOIN pg_catalog.pg_tablespace t "
+						 "     ON c.reltablespace = t.oid "
+						 "ORDER BY 1;");
+
+	return query.data;
+}
 
-	i_reloid = PQfnumber(res, "reloid");
-	i_indtable = PQfnumber(res, "indtable");
-	i_toastheap = PQfnumber(res, "toastheap");
-	i_nspname = PQfnumber(res, "nspname");
-	i_relname = PQfnumber(res, "relname");
-	i_relfilenumber = PQfnumber(res, "relfilenode");
-	i_reltablespace = PQfnumber(res, "reltablespace");
-	i_spclocation = PQfnumber(res, "spclocation");
+static void
+get_rel_infos_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	int			ntups = PQntuples(res);
+	RelInfo    *relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	int			i_reloid = PQfnumber(res, "reloid");
+	int			i_indtable = PQfnumber(res, "indtable");
+	int			i_toastheap = PQfnumber(res, "toastheap");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+	int			i_relfilenumber = PQfnumber(res, "relfilenode");
+	int			i_reltablespace = PQfnumber(res, "reltablespace");
+	int			i_spclocation = PQfnumber(res, "spclocation");
+	int			num_rels = 0;
+	char	   *nspname = NULL;
+	char	   *relname = NULL;
+	char	   *tablespace = NULL;
+	char	   *last_namespace = NULL;
+	char	   *last_tablespace = NULL;
 
-	for (relnum = 0; relnum < ntups; relnum++)
+	for (int relnum = 0; relnum < ntups; relnum++)
 	{
 		RelInfo    *curr = &relinfos[num_rels++];
 
@@ -610,9 +605,6 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 			/* A zero reltablespace oid indicates the database tablespace. */
 			curr->tablespace = dbinfo->db_tablespace;
 	}
-	PQclear(res);
-
-	PQfinish(conn);
 
 	dbinfo->rel_arr.rels = relinfos;
 	dbinfo->rel_arr.nrels = num_rels;
@@ -634,20 +626,9 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
  * check_old_cluster_for_valid_slots() would raise a FATAL error if such slots
  * are included.
  */
-static void
-get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
+static char *
+get_old_cluster_logical_slot_infos_query(void *arg)
 {
-	PGconn	   *conn;
-	PGresult   *res;
-	LogicalSlotInfo *slotinfos = NULL;
-	int			num_slots;
-
-	/* Logical slots can be migrated since PG17. */
-	if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1600)
-		return;
-
-	conn = connectToServer(&old_cluster, dbinfo->db_name);
-
 	/*
 	 * Fetch the logical replication slot information. The check whether the
 	 * slot is considered caught up is done by an upgrade function. This
@@ -665,18 +646,23 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
-							"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
-							"FROM pg_catalog.pg_replication_slots "
-							"WHERE slot_type = 'logical' AND "
-							"database = current_database() AND "
-							"temporary IS FALSE;",
-							user_opts.live_check ? "FALSE" :
-							"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
-							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
-							"END)");
-
-	num_slots = PQntuples(res);
+	return psprintf("SELECT slot_name, plugin, two_phase, failover, "
+					"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
+					"FROM pg_catalog.pg_replication_slots "
+					"WHERE slot_type = 'logical' AND "
+					"database = current_database() AND "
+					"temporary IS FALSE;",
+					user_opts.live_check ? "FALSE" :
+					"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
+					"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
+					"END)");
+}
+
+static void
+get_old_cluster_logical_slot_infos_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	LogicalSlotInfo *slotinfos = NULL;
+	int			num_slots = PQntuples(res);
 
 	if (num_slots)
 	{
@@ -709,14 +695,10 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 		}
 	}
 
-	PQclear(res);
-	PQfinish(conn);
-
 	dbinfo->slot_arr.slots = slotinfos;
 	dbinfo->slot_arr.nslots = num_slots;
 }
 
-
 /*
  * count_old_cluster_logical_slots()
  *
-- 
2.39.3 (Apple Git-146)

v7-0004-use-new-pg_upgrade-async-API-to-parallelize-getti.patchtext/plain; charset=us-asciiDownload
From a02676370bc1a0e209f5a17ba5eddeaa1d71ac46 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:24:35 -0500
Subject: [PATCH v7 04/11] use new pg_upgrade async API to parallelize getting
 loadable libraries

---
 src/bin/pg_upgrade/function.c | 63 ++++++++++++++++++++---------------
 1 file changed, 37 insertions(+), 26 deletions(-)

diff --git a/src/bin/pg_upgrade/function.c b/src/bin/pg_upgrade/function.c
index 7e3abed098..da3717e71c 100644
--- a/src/bin/pg_upgrade/function.c
+++ b/src/bin/pg_upgrade/function.c
@@ -42,6 +42,32 @@ library_name_compare(const void *p1, const void *p2)
 					  ((const LibraryInfo *) p2)->dbnum);
 }
 
+struct loadable_libraries_state
+{
+	PGresult  **ress;
+	int			totaltups;
+};
+
+static char *
+get_loadable_libraries_query(void *arg)
+{
+	return psprintf("SELECT DISTINCT probin "
+					"FROM pg_catalog.pg_proc "
+					"WHERE prolang = %u AND "
+					"probin IS NOT NULL AND "
+					"oid >= %u;",
+					ClanguageId,
+					FirstNormalObjectId);
+}
+
+static void
+get_loadable_libraries_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct loadable_libraries_state *state = (struct loadable_libraries_state *) arg;
+
+	state->ress[dbinfo - old_cluster.dbarr.dbs] = res;
+	state->totaltups += PQntuples(res);
+}
 
 /*
  * get_loadable_libraries()
@@ -54,47 +80,32 @@ library_name_compare(const void *p1, const void *p2)
 void
 get_loadable_libraries(void)
 {
-	PGresult  **ress;
 	int			totaltups;
 	int			dbnum;
 	int			n_libinfos;
+	AsyncTask  *task = async_task_create();
+	struct loadable_libraries_state state;
 
-	ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
-	totaltups = 0;
+	state.ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
+	state.totaltups = 0;
 
-	/* Fetch all library names, removing duplicates within each DB */
-	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
+	async_task_add_step(task, get_loadable_libraries_query,
+						get_loadable_libraries_result, false, &state);
 
-		/*
-		 * Fetch all libraries containing non-built-in C functions in this DB.
-		 */
-		ress[dbnum] = executeQueryOrDie(conn,
-										"SELECT DISTINCT probin "
-										"FROM pg_catalog.pg_proc "
-										"WHERE prolang = %u AND "
-										"probin IS NOT NULL AND "
-										"oid >= %u;",
-										ClanguageId,
-										FirstNormalObjectId);
-		totaltups += PQntuples(ress[dbnum]);
-
-		PQfinish(conn);
-	}
+	async_task_run(task, &old_cluster);
+	async_task_free(task);
 
 	/*
 	 * Allocate memory for required libraries and logical replication output
 	 * plugins.
 	 */
-	n_libinfos = totaltups + count_old_cluster_logical_slots();
+	n_libinfos = state.totaltups + count_old_cluster_logical_slots();
 	os_info.libraries = (LibraryInfo *) pg_malloc(sizeof(LibraryInfo) * n_libinfos);
 	totaltups = 0;
 
 	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
 	{
-		PGresult   *res = ress[dbnum];
+		PGresult   *res = state.ress[dbnum];
 		int			ntups;
 		int			rowno;
 		LogicalSlotInfoArr *slot_arr = &old_cluster.dbarr.dbs[dbnum].slot_arr;
@@ -129,7 +140,7 @@ get_loadable_libraries(void)
 		}
 	}
 
-	pg_free(ress);
+	pg_free(state.ress);
 
 	os_info.num_libraries = totaltups;
 }
-- 
2.39.3 (Apple Git-146)

v7-0005-use-new-pg_upgrade-async-API-to-parallelize-repor.patchtext/plain; charset=us-asciiDownload
From fa707bc6c8e299579006246b164375080f40cced Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:31:57 -0500
Subject: [PATCH v7 05/11] use new pg_upgrade async API to parallelize
 reporting extension updates

---
 src/bin/pg_upgrade/version.c | 82 ++++++++++++++++++------------------
 1 file changed, 41 insertions(+), 41 deletions(-)

diff --git a/src/bin/pg_upgrade/version.c b/src/bin/pg_upgrade/version.c
index 2de6dffccd..bfcf08c936 100644
--- a/src/bin/pg_upgrade/version.c
+++ b/src/bin/pg_upgrade/version.c
@@ -139,6 +139,42 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 		check_ok();
 }
 
+static char *
+report_extension_updates_query(void *arg)
+{
+	return pg_strdup("SELECT name "
+					 "FROM pg_available_extensions "
+					 "WHERE installed_version != default_version");
+}
+
+static void
+report_extension_updates_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_name = PQfnumber(res, "name");
+	char	   *output_path = "update_extensions.sql";
+	FILE	  **script = (FILE **) arg;
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL && (*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			PQExpBufferData connectbuf;
+
+			initPQExpBuffer(&connectbuf);
+			appendPsqlMetaConnect(&connectbuf, dbinfo->db_name);
+			fputs(connectbuf.data, *script);
+			termPQExpBuffer(&connectbuf);
+			db_used = true;
+		}
+		fprintf(*script, "ALTER EXTENSION %s UPDATE;\n",
+				quote_identifier(PQgetvalue(res, rowno, i_name)));
+	}
+}
+
 /*
  * report_extension_updates()
  *	Report extensions that should be updated.
@@ -146,53 +182,17 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 void
 report_extension_updates(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char	   *output_path = "update_extensions.sql";
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for extension updates");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_name;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* find extensions needing updates */
-		res = executeQueryOrDie(conn,
-								"SELECT name "
-								"FROM pg_available_extensions "
-								"WHERE installed_version != default_version"
-			);
-
-		ntups = PQntuples(res);
-		i_name = PQfnumber(res, "name");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				PQExpBufferData connectbuf;
-
-				initPQExpBuffer(&connectbuf);
-				appendPsqlMetaConnect(&connectbuf, active_db->db_name);
-				fputs(connectbuf.data, script);
-				termPQExpBuffer(&connectbuf);
-				db_used = true;
-			}
-			fprintf(script, "ALTER EXTENSION %s UPDATE;\n",
-					quote_identifier(PQgetvalue(res, rowno, i_name)));
-		}
+	async_task_add_step(task, report_extension_updates_query,
+						report_extension_updates_result, true, &script);
 
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v7-0006-parallelize-data-type-checks-in-pg_upgrade.patchtext/plain; charset=us-asciiDownload
From 6728dd328063306ce9f7d7b71dc7f6bf98abae04 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Sat, 6 Jul 2024 21:06:31 -0500
Subject: [PATCH v7 06/11] parallelize data type checks in pg_upgrade

---
 src/bin/pg_upgrade/check.c | 319 +++++++++++++++++++------------------
 1 file changed, 160 insertions(+), 159 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 44b8f16df9..4c78fe0274 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -32,6 +32,10 @@ static void check_new_cluster_subscription_configuration(void);
 static void check_old_cluster_for_valid_slots(void);
 static void check_old_cluster_subscription_state(void);
 
+static bool *data_type_check_results;
+static bool data_type_check_failed;
+static PQExpBufferData data_type_check_report;
+
 /*
  * DataTypesUsageChecks - definitions of data type checks for the old cluster
  * in order to determine if an upgrade can be performed.  See the comment on
@@ -314,6 +318,129 @@ static DataTypesUsageChecks data_types_usage_checks[] =
 	}
 };
 
+static char *
+data_type_check_query(void *arg)
+{
+	int			i = (int) (intptr_t) arg;
+	DataTypesUsageChecks *check = &data_types_usage_checks[i];
+
+	return psprintf("WITH RECURSIVE oids AS ( "
+	/* start with the type(s) returned by base_query */
+					"	%s "
+					"	UNION ALL "
+					"	SELECT * FROM ( "
+	/* inner WITH because we can only reference the CTE once */
+					"		WITH x AS (SELECT oid FROM oids) "
+	/* domains on any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
+					"			UNION ALL "
+	/* arrays over any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
+					"			UNION ALL "
+	/* composite types containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
+					"			WHERE t.typtype = 'c' AND "
+					"				  t.oid = c.reltype AND "
+					"				  c.oid = a.attrelid AND "
+					"				  NOT a.attisdropped AND "
+					"				  a.atttypid = x.oid "
+					"			UNION ALL "
+	/* ranges containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
+					"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
+					"	) foo "
+					") "
+	/* now look for stored columns of any such type */
+					"SELECT n.nspname, c.relname, a.attname "
+					"FROM	pg_catalog.pg_class c, "
+					"		pg_catalog.pg_namespace n, "
+					"		pg_catalog.pg_attribute a "
+					"WHERE	c.oid = a.attrelid AND "
+					"		NOT a.attisdropped AND "
+					"		a.atttypid IN (SELECT oid FROM oids) AND "
+					"		c.relkind IN ("
+					CppAsString2(RELKIND_RELATION) ", "
+					CppAsString2(RELKIND_MATVIEW) ", "
+					CppAsString2(RELKIND_INDEX) ") AND "
+					"		c.relnamespace = n.oid AND "
+	/* exclude possible orphaned temp tables */
+					"		n.nspname !~ '^pg_temp_' AND "
+					"		n.nspname !~ '^pg_toast_temp_' AND "
+	/* exclude system catalogs, too */
+					"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
+					check->base_query);
+}
+
+static void
+data_type_check_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	int			i = (int) (intptr_t) arg;
+	DataTypesUsageChecks *check = &data_types_usage_checks[i];
+	int			ntups = PQntuples(res);
+
+	if (ntups)
+	{
+		char		output_path[MAXPGPATH];
+		int			i_nspname;
+		int			i_relname;
+		int			i_attname;
+		FILE	   *script = NULL;
+		bool		db_used = false;
+
+		snprintf(output_path, sizeof(output_path), "%s/%s",
+				 log_opts.basedir,
+				 check->report_filename);
+
+		/*
+		 * Make sure we have a buffer to save reports to now that we found a
+		 * first failing check.
+		 */
+		if (!data_type_check_failed)
+			initPQExpBuffer(&data_type_check_report);
+		data_type_check_failed = true;
+
+		/*
+		 * If this is the first time we see an error for the check in question
+		 * then print a status message of the failure.
+		 */
+		if (!data_type_check_results[i])
+		{
+			pg_log(PG_REPORT, "    failed check: %s", _(check->status));
+			appendPQExpBuffer(&data_type_check_report, "\n%s\n%s    %s\n",
+							  _(check->report_text),
+							  _("A list of the problem columns is in the file:"),
+							  output_path);
+		}
+		data_type_check_results[i] = true;
+
+		i_nspname = PQfnumber(res, "nspname");
+		i_relname = PQfnumber(res, "relname");
+		i_attname = PQfnumber(res, "attname");
+
+		for (int rowno = 0; rowno < ntups; rowno++)
+		{
+			if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
+				pg_fatal("could not open file \"%s\": %m", output_path);
+
+			if (!db_used)
+			{
+				fprintf(script, "In database: %s\n", dbinfo->db_name);
+				db_used = true;
+			}
+			fprintf(script, "  %s.%s.%s\n",
+					PQgetvalue(res, rowno, i_nspname),
+					PQgetvalue(res, rowno, i_relname),
+					PQgetvalue(res, rowno, i_attname));
+		}
+
+		if (script)
+		{
+			fclose(script);
+			script = NULL;
+		}
+	}
+}
+
 /*
  * check_for_data_types_usage()
  *	Detect whether there are any stored columns depending on given type(s)
@@ -337,10 +464,9 @@ static void
 check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
 {
 	bool		found = false;
-	bool	   *results;
-	PQExpBufferData report;
 	DataTypesUsageChecks *tmp = checks;
 	int			n_data_types_usage_checks = 0;
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for data type usage");
 
@@ -352,176 +478,51 @@ check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
 	}
 
 	/* Prepare an array to store the results of checks in */
-	results = pg_malloc0(sizeof(bool) * n_data_types_usage_checks);
+	data_type_check_results = pg_malloc0(sizeof(bool) * n_data_types_usage_checks);
+	data_type_check_failed = false;
 
-	/*
-	 * Connect to each database in the cluster and run all defined checks
-	 * against that database before trying the next one.
-	 */
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int i = 0; i < n_data_types_usage_checks; i++)
 	{
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
+		DataTypesUsageChecks *check = &checks[i];
 
-		for (int checknum = 0; checknum < n_data_types_usage_checks; checknum++)
+		if (check->threshold_version == MANUAL_CHECK)
 		{
-			PGresult   *res;
-			int			ntups;
-			int			i_nspname;
-			int			i_relname;
-			int			i_attname;
-			FILE	   *script = NULL;
-			bool		db_used = false;
-			char		output_path[MAXPGPATH];
-			DataTypesUsageChecks *cur_check = &checks[checknum];
-
-			if (cur_check->threshold_version == MANUAL_CHECK)
-			{
-				Assert(cur_check->version_hook);
-
-				/*
-				 * Make sure that the check applies to the current cluster
-				 * version and skip if not. If no check hook has been defined
-				 * we run the check for all versions.
-				 */
-				if (!cur_check->version_hook(cluster))
-					continue;
-			}
-			else if (cur_check->threshold_version != ALL_VERSIONS)
-			{
-				if (GET_MAJOR_VERSION(cluster->major_version) > cur_check->threshold_version)
-					continue;
-			}
-			else
-				Assert(cur_check->threshold_version == ALL_VERSIONS);
-
-			snprintf(output_path, sizeof(output_path), "%s/%s",
-					 log_opts.basedir,
-					 cur_check->report_filename);
-
-			/*
-			 * The type(s) of interest might be wrapped in a domain, array,
-			 * composite, or range, and these container types can be nested
-			 * (to varying extents depending on server version, but that's not
-			 * of concern here).  To handle all these cases we need a
-			 * recursive CTE.
-			 */
-			res = executeQueryOrDie(conn,
-									"WITH RECURSIVE oids AS ( "
-			/* start with the type(s) returned by base_query */
-									"	%s "
-									"	UNION ALL "
-									"	SELECT * FROM ( "
-			/* inner WITH because we can only reference the CTE once */
-									"		WITH x AS (SELECT oid FROM oids) "
-			/* domains on any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
-									"			UNION ALL "
-			/* arrays over any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
-									"			UNION ALL "
-			/* composite types containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
-									"			WHERE t.typtype = 'c' AND "
-									"				  t.oid = c.reltype AND "
-									"				  c.oid = a.attrelid AND "
-									"				  NOT a.attisdropped AND "
-									"				  a.atttypid = x.oid "
-									"			UNION ALL "
-			/* ranges containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
-									"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
-									"	) foo "
-									") "
-			/* now look for stored columns of any such type */
-									"SELECT n.nspname, c.relname, a.attname "
-									"FROM	pg_catalog.pg_class c, "
-									"		pg_catalog.pg_namespace n, "
-									"		pg_catalog.pg_attribute a "
-									"WHERE	c.oid = a.attrelid AND "
-									"		NOT a.attisdropped AND "
-									"		a.atttypid IN (SELECT oid FROM oids) AND "
-									"		c.relkind IN ("
-									CppAsString2(RELKIND_RELATION) ", "
-									CppAsString2(RELKIND_MATVIEW) ", "
-									CppAsString2(RELKIND_INDEX) ") AND "
-									"		c.relnamespace = n.oid AND "
-			/* exclude possible orphaned temp tables */
-									"		n.nspname !~ '^pg_temp_' AND "
-									"		n.nspname !~ '^pg_toast_temp_' AND "
-			/* exclude system catalogs, too */
-									"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
-									cur_check->base_query);
-
-			ntups = PQntuples(res);
+			Assert(check->version_hook);
 
 			/*
-			 * The datatype was found, so extract the data and log to the
-			 * requested filename. We need to open the file for appending
-			 * since the check might have already found the type in another
-			 * database earlier in the loop.
+			 * Make sure that the check applies to the current cluster version
+			 * and skip it if not.
 			 */
-			if (ntups)
-			{
-				/*
-				 * Make sure we have a buffer to save reports to now that we
-				 * found a first failing check.
-				 */
-				if (!found)
-					initPQExpBuffer(&report);
-				found = true;
-
-				/*
-				 * If this is the first time we see an error for the check in
-				 * question then print a status message of the failure.
-				 */
-				if (!results[checknum])
-				{
-					pg_log(PG_REPORT, "    failed check: %s", _(cur_check->status));
-					appendPQExpBuffer(&report, "\n%s\n%s    %s\n",
-									  _(cur_check->report_text),
-									  _("A list of the problem columns is in the file:"),
-									  output_path);
-				}
-				results[checknum] = true;
-
-				i_nspname = PQfnumber(res, "nspname");
-				i_relname = PQfnumber(res, "relname");
-				i_attname = PQfnumber(res, "attname");
-
-				for (int rowno = 0; rowno < ntups; rowno++)
-				{
-					if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
-						pg_fatal("could not open file \"%s\": %m", output_path);
-
-					if (!db_used)
-					{
-						fprintf(script, "In database: %s\n", active_db->db_name);
-						db_used = true;
-					}
-					fprintf(script, "  %s.%s.%s\n",
-							PQgetvalue(res, rowno, i_nspname),
-							PQgetvalue(res, rowno, i_relname),
-							PQgetvalue(res, rowno, i_attname));
-				}
-
-				if (script)
-				{
-					fclose(script);
-					script = NULL;
-				}
-			}
-
-			PQclear(res);
+			if (!check->version_hook(cluster))
+				continue;
 		}
+		else if (check->threshold_version != ALL_VERSIONS)
+		{
+			if (GET_MAJOR_VERSION(cluster->major_version) > check->threshold_version)
+				continue;
+		}
+		else
+			Assert(check->threshold_version == ALL_VERSIONS);
 
-		PQfinish(conn);
+		async_task_add_step(task, data_type_check_query,
+							data_type_check_process, true,
+							(void *) (intptr_t) i);
 	}
 
+	/*
+	 * Connect to each database in the cluster and run all defined checks
+	 * against that database before trying the next one.
+	 */
+	async_task_run(task, cluster);
+	async_task_free(task);
+
 	if (found)
-		pg_fatal("Data type checks failed: %s", report.data);
+	{
+		pg_fatal("Data type checks failed: %s", data_type_check_report.data);
+		termPQExpBuffer(&data_type_check_report);
+	}
 
-	pg_free(results);
+	pg_free(data_type_check_results);
 
 	check_ok();
 }
-- 
2.39.3 (Apple Git-146)

v7-0007-parallelize-isn-and-int8-passing-mismatch-check-i.patchtext/plain; charset=us-asciiDownload
From 3f8753ef00a6026d72c0e12aee2f7b162f6af08e Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:00:20 -0500
Subject: [PATCH v7 07/11] parallelize isn and int8 passing mismatch check in
 pg_upgrade

---
 src/bin/pg_upgrade/check.c | 85 ++++++++++++++++++++------------------
 1 file changed, 44 insertions(+), 41 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 4c78fe0274..311743dd1e 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1195,6 +1195,44 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 	check_ok();
 }
 
+static char *
+isn_and_int8_passing_mismatch_query(void *arg)
+{
+	return pg_strdup("SELECT n.nspname, p.proname "
+					 "FROM   pg_catalog.pg_proc p, "
+					 "       pg_catalog.pg_namespace n "
+					 "WHERE  p.pronamespace = n.oid AND "
+					 "       p.probin = '$libdir/isn'");
+}
+
+static void
+isn_and_int8_passing_mismatch_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_proname = PQfnumber(res, "proname");
+	FILE	  **script = (FILE **) arg;
+	char		output_path[MAXPGPATH];
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "contrib_isn_and_int8_pass_by_value.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL && (*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_proname));
+	}
+}
 
 /*
  *	check_for_isn_and_int8_passing_mismatch()
@@ -1206,8 +1244,8 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 static void
 check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
+	AsyncTask  *task;
 	char		output_path[MAXPGPATH];
 
 	prep_status("Checking for contrib/isn with bigint-passing mismatch");
@@ -1224,46 +1262,11 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "contrib_isn_and_int8_pass_by_value.txt");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_proname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* Find any functions coming from contrib/isn */
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, p.proname "
-								"FROM	pg_catalog.pg_proc p, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	p.pronamespace = n.oid AND "
-								"		p.probin = '$libdir/isn'");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_proname = PQfnumber(res, "proname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_proname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	task = async_task_create();
+	async_task_add_step(task, isn_and_int8_passing_mismatch_query,
+						isn_and_int8_passing_mismatch_process, true, &script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v7-0008-parallelize-user-defined-postfix-ops-check-in-pg_.patchtext/plain; charset=us-asciiDownload
From b53af8ba256540bad858d675940a352a2a514d9b Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:12:49 -0500
Subject: [PATCH v7 08/11] parallelize user defined postfix ops check in
 pg_upgrade

---
 src/bin/pg_upgrade/check.c | 134 +++++++++++++++++++------------------
 1 file changed, 69 insertions(+), 65 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 311743dd1e..37baf1ae27 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1284,15 +1284,79 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 		check_ok();
 }
 
+static char *
+user_defined_postfix_ops_query(void *arg)
+{
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	return pg_strdup("SELECT o.oid AS oproid, "
+					 "       n.nspname AS oprnsp, "
+					 "       o.oprname, "
+					 "       tn.nspname AS typnsp, "
+					 "       t.typname "
+					 "FROM pg_catalog.pg_operator o, "
+					 "     pg_catalog.pg_namespace n, "
+					 "     pg_catalog.pg_type t, "
+					 "     pg_catalog.pg_namespace tn "
+					 "WHERE o.oprnamespace = n.oid AND "
+					 "      o.oprleft = t.oid AND "
+					 "      t.typnamespace = tn.oid AND "
+					 "      o.oprright = 0 AND "
+					 "      o.oid >= 16384");
+}
+
+static void
+user_defined_postfix_ops_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	FILE	  **script = (FILE **) arg;
+	char		output_path[MAXPGPATH];
+	int			ntups = PQntuples(res);
+	bool		db_used = false;
+	int			i_oproid = PQfnumber(res, "oproid");
+	int			i_oprnsp = PQfnumber(res, "oprnsp");
+	int			i_oprname = PQfnumber(res, "oprname");
+	int			i_typnsp = PQfnumber(res, "typnsp");
+	int			i_typname = PQfnumber(res, "typname");
+
+	if (!ntups)
+		return;
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "postfix_ops.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL &&
+			(*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
+				PQgetvalue(res, rowno, i_oproid),
+				PQgetvalue(res, rowno, i_oprnsp),
+				PQgetvalue(res, rowno, i_oprname),
+				PQgetvalue(res, rowno, i_typnsp),
+				PQgetvalue(res, rowno, i_typname));
+	}
+}
+
 /*
  * Verify that no user defined postfix operators exist.
  */
 static void
 check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for user-defined postfix operators");
 
@@ -1300,70 +1364,10 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "postfix_ops.txt");
 
-	/* Find any user defined postfix operators */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_oproid,
-					i_oprnsp,
-					i_oprname,
-					i_typnsp,
-					i_typname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT o.oid AS oproid, "
-								"       n.nspname AS oprnsp, "
-								"       o.oprname, "
-								"       tn.nspname AS typnsp, "
-								"       t.typname "
-								"FROM pg_catalog.pg_operator o, "
-								"     pg_catalog.pg_namespace n, "
-								"     pg_catalog.pg_type t, "
-								"     pg_catalog.pg_namespace tn "
-								"WHERE o.oprnamespace = n.oid AND "
-								"      o.oprleft = t.oid AND "
-								"      t.typnamespace = tn.oid AND "
-								"      o.oprright = 0 AND "
-								"      o.oid >= 16384");
-		ntups = PQntuples(res);
-		i_oproid = PQfnumber(res, "oproid");
-		i_oprnsp = PQfnumber(res, "oprnsp");
-		i_oprname = PQfnumber(res, "oprname");
-		i_typnsp = PQfnumber(res, "typnsp");
-		i_typname = PQfnumber(res, "typname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
-					PQgetvalue(res, rowno, i_oproid),
-					PQgetvalue(res, rowno, i_oprnsp),
-					PQgetvalue(res, rowno, i_oprname),
-					PQgetvalue(res, rowno, i_typnsp),
-					PQgetvalue(res, rowno, i_typname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_add_step(task, user_defined_postfix_ops_query,
+						user_defined_postfix_ops_process, true, &script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v7-0009-parallelize-incompatible-polymorphics-check-in-pg.patchtext/plain; charset=us-asciiDownload
From e8e75b83ed43b81b2772335515a43184102babdb Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:30:19 -0500
Subject: [PATCH v7 09/11] parallelize incompatible polymorphics check in
 pg_upgrade

---
 src/bin/pg_upgrade/check.c       | 174 ++++++++++++++++---------------
 src/tools/pgindent/typedefs.list |   1 +
 2 files changed, 92 insertions(+), 83 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 37baf1ae27..32c29aa881 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1383,6 +1383,81 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 		check_ok();
 }
 
+typedef struct incompat_polymorphics_state
+{
+	FILE	   *script;
+	PQExpBufferData old_polymorphics;
+	char		output_path[MAXPGPATH];
+} incompat_polymorphics_state;
+
+static char *
+incompat_polymorphics_query(void *arg)
+{
+	incompat_polymorphics_state *state = (incompat_polymorphics_state *) arg;
+
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	/* Aggregate transition functions */
+	return psprintf("SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					"FROM pg_proc AS p "
+					"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					"JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
+					"WHERE p.oid >= 16384 "
+					"AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
+					"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Aggregate final functions */
+					"UNION ALL "
+					"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					"FROM pg_proc AS p "
+					"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					"JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
+					"WHERE p.oid >= 16384 "
+					"AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
+					"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Operators */
+					"UNION ALL "
+					"SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
+					"FROM pg_operator AS op "
+					"WHERE op.oid >= 16384 "
+					"AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
+					"AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
+					state->old_polymorphics.data,
+					state->old_polymorphics.data,
+					state->old_polymorphics.data);
+}
+
+static void
+incompat_polymorphics_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	incompat_polymorphics_state *state = (incompat_polymorphics_state *) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_objkind = PQfnumber(res, "objkind");
+	int			i_objname = PQfnumber(res, "objname");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (state->script == NULL &&
+			(state->script = fopen_priv(state->output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state->output_path);
+		if (!db_used)
+		{
+			fprintf(state->script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+
+		fprintf(state->script, "  %s: %s\n",
+				PQgetvalue(res, rowno, i_objkind),
+				PQgetvalue(res, rowno, i_objname));
+	}
+}
+
 /*
  *	check_for_incompatible_polymorphics()
  *
@@ -1392,111 +1467,44 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 static void
 check_for_incompatible_polymorphics(ClusterInfo *cluster)
 {
-	PGresult   *res;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
-	PQExpBufferData old_polymorphics;
+	AsyncTask  *task = async_task_create();
+	incompat_polymorphics_state state;
 
 	prep_status("Checking for incompatible polymorphic functions");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	state.script = NULL;
+	snprintf(state.output_path, sizeof(state.output_path), "%s/%s",
 			 log_opts.basedir,
 			 "incompatible_polymorphics.txt");
 
 	/* The set of problematic functions varies a bit in different versions */
-	initPQExpBuffer(&old_polymorphics);
+	initPQExpBuffer(&state.old_polymorphics);
 
-	appendPQExpBufferStr(&old_polymorphics,
+	appendPQExpBufferStr(&state.old_polymorphics,
 						 "'array_append(anyarray,anyelement)'"
 						 ", 'array_cat(anyarray,anyarray)'"
 						 ", 'array_prepend(anyelement,anyarray)'");
 
 	if (GET_MAJOR_VERSION(cluster->major_version) >= 903)
-		appendPQExpBufferStr(&old_polymorphics,
+		appendPQExpBufferStr(&state.old_polymorphics,
 							 ", 'array_remove(anyarray,anyelement)'"
 							 ", 'array_replace(anyarray,anyelement,anyelement)'");
 
 	if (GET_MAJOR_VERSION(cluster->major_version) >= 905)
-		appendPQExpBufferStr(&old_polymorphics,
+		appendPQExpBufferStr(&state.old_polymorphics,
 							 ", 'array_position(anyarray,anyelement)'"
 							 ", 'array_position(anyarray,anyelement,integer)'"
 							 ", 'array_positions(anyarray,anyelement)'"
 							 ", 'width_bucket(anyelement,anyarray)'");
 
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		bool		db_used = false;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-		int			ntups;
-		int			i_objkind,
-					i_objname;
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-		/* Aggregate transition functions */
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Aggregate final functions */
-								"UNION ALL "
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Operators */
-								"UNION ALL "
-								"SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
-								"FROM pg_operator AS op "
-								"WHERE op.oid >= 16384 "
-								"AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
-								old_polymorphics.data,
-								old_polymorphics.data,
-								old_polymorphics.data);
-
-		ntups = PQntuples(res);
-
-		i_objkind = PQfnumber(res, "objkind");
-		i_objname = PQfnumber(res, "objname");
-
-		for (int rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-
-			fprintf(script, "  %s: %s\n",
-					PQgetvalue(res, rowno, i_objkind),
-					PQgetvalue(res, rowno, i_objname));
-		}
-
-		PQclear(res);
-		PQfinish(conn);
-	}
+	async_task_add_step(task, incompat_polymorphics_query,
+						incompat_polymorphics_process, true, &state);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
-	if (script)
+	if (state.script)
 	{
-		fclose(script);
+		fclose(state.script);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains user-defined objects that refer to internal\n"
 				 "polymorphic functions with arguments of type \"anyarray\" or \"anyelement\".\n"
@@ -1504,12 +1512,12 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 				 "afterwards, changing them to refer to the new corresponding functions with\n"
 				 "arguments of type \"anycompatiblearray\" and \"anycompatible\".\n"
 				 "A list of the problematic objects is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", state.output_path);
 	}
 	else
 		check_ok();
 
-	termPQExpBuffer(&old_polymorphics);
+	termPQExpBuffer(&state.old_polymorphics);
 }
 
 /*
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 2c43a7adcc..90c4a25e7f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3539,6 +3539,7 @@ hstoreUpgrade_t
 hyperLogLogState
 ifState
 import_error_callback_arg
+incompat_polymorphics_state
 indexed_tlist
 inet
 inetKEY
-- 
2.39.3 (Apple Git-146)

v7-0010-parallelize-tables-with-oids-check-in-pg_upgrade.patchtext/plain; charset=us-asciiDownload
From e5275dce0cb482ee8fed52a9c797f5182b40a942 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:42:22 -0500
Subject: [PATCH v7 10/11] parallelize tables with oids check in pg_upgrade

---
 src/bin/pg_upgrade/check.c | 90 ++++++++++++++++++++------------------
 1 file changed, 48 insertions(+), 42 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 32c29aa881..410d8b0cad 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1520,15 +1520,58 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 	termPQExpBuffer(&state.old_polymorphics);
 }
 
+static char *
+with_oids_query(void *arg)
+{
+	return pg_strdup("SELECT n.nspname, c.relname "
+					 "FROM   pg_catalog.pg_class c, "
+					 "       pg_catalog.pg_namespace n "
+					 "WHERE  c.relnamespace = n.oid AND "
+					 "       c.relhasoids AND"
+					 "       n.nspname NOT IN ('pg_catalog')");
+}
+
+static void
+with_oids_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	char		output_path[MAXPGPATH];
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+	FILE	  **script = (FILE **) arg;
+
+	if (!ntups)
+		return;
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "tables_with_oids.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL && (*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_relname));
+	}
+}
+
 /*
  * Verify that no tables are declared WITH OIDS.
  */
 static void
 check_for_tables_with_oids(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for tables WITH OIDS");
 
@@ -1536,47 +1579,10 @@ check_for_tables_with_oids(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "tables_with_oids.txt");
 
-	/* Find any tables declared WITH OIDS */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_relname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, c.relname "
-								"FROM	pg_catalog.pg_class c, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	c.relnamespace = n.oid AND "
-								"		c.relhasoids AND"
-								"       n.nspname NOT IN ('pg_catalog')");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_relname = PQfnumber(res, "relname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_relname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_add_step(task, with_oids_query,
+						with_oids_process, true, &script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v7-0011-parallelize-user-defined-encoding-conversions-che.patchtext/plain; charset=us-asciiDownload
From bf4634ae763c8e39119561a8d5d5c17673b546d8 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:52:13 -0500
Subject: [PATCH v7 11/11] parallelize user defined encoding conversions check
 in pg_upgrade

---
 src/bin/pg_upgrade/check.c | 107 ++++++++++++++++++++-----------------
 1 file changed, 57 insertions(+), 50 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 410d8b0cad..f81c994339 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1656,15 +1656,66 @@ check_for_pg_role_prefix(ClusterInfo *cluster)
 		check_ok();
 }
 
+static char *
+user_defined_encoding_conversions_query(void *arg)
+{
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	return pstrdup("SELECT c.oid as conoid, c.conname, n.nspname "
+				   "FROM pg_catalog.pg_conversion c, "
+				   "     pg_catalog.pg_namespace n "
+				   "WHERE c.connamespace = n.oid AND "
+				   "      c.oid >= 16384");
+}
+
+static void
+user_defined_encoding_conversions_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	FILE	  **script = (FILE **) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	char		output_path[MAXPGPATH];
+	int			i_conoid = PQfnumber(res, "conoid");
+	int			i_conname = PQfnumber(res, "conname");
+	int			i_nspname = PQfnumber(res, "nspname");
+
+	if (!ntups)
+		return;
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "encoding_conversions.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL &&
+			(*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  (oid=%s) %s.%s\n",
+				PQgetvalue(res, rowno, i_conoid),
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_conname));
+	}
+}
+
 /*
  * Verify that no user-defined encoding conversions exist.
  */
 static void
 check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
 
 	prep_status("Checking for user-defined encoding conversions");
 
@@ -1672,55 +1723,11 @@ check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "encoding_conversions.txt");
 
-	/* Find any user defined encoding conversions */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_conoid,
-					i_conname,
-					i_nspname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT c.oid as conoid, c.conname, n.nspname "
-								"FROM pg_catalog.pg_conversion c, "
-								"     pg_catalog.pg_namespace n "
-								"WHERE c.connamespace = n.oid AND "
-								"      c.oid >= 16384");
-		ntups = PQntuples(res);
-		i_conoid = PQfnumber(res, "conoid");
-		i_conname = PQfnumber(res, "conname");
-		i_nspname = PQfnumber(res, "nspname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s\n",
-					PQgetvalue(res, rowno, i_conoid),
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_conname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_add_step(task, user_defined_encoding_conversions_query,
+						user_defined_encoding_conversions_process, true,
+						&script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

#20Ilya Gladyshev
ilya.v.gladyshev@gmail.com
In reply to: Nathan Bossart (#19)
Re: optimizing pg_upgrade's once-in-each-database steps

On 01.08.2024 22:41, Nathan Bossart wrote:

Here is a new patch set. Besides rebasing, I've added the recursive call
to process_slot() mentioned in the quoted text, and I've added quite a bit
of commentary to async.c.

That's much better now, thanks! Here's my code review, note that I
haven't tested the patches yet:

+void
+async_task_add_step(AsyncTask *task,
+                    AsyncTaskGetQueryCB query_cb,
+                    AsyncTaskProcessCB process_cb, bool free_result,
+                    void *arg)

Is there any reason to have query as a callback function instead of char
*? From what I see right now, it doesn't give any extra flexibility, as
the query has to be static anyway (can't be customized on a per-database
basis) and will be created once before all the callbacks are run. While
passing in char * makes the API simpler, excludes any potential error of
making the query dependent on the current database and removes the
unnecessary malloc/free of the static strings.

+static void
+dispatch_query(const ClusterInfo *cluster, AsyncSlot *slot,
+               const AsyncTask *task)
+{
+    ...
+    if (!PQsendQuery(slot->conn, cbs->query))
+        conn_failure(slot->conn);
+}

This will print "connection failure: connection pointer is NULL", which
I don't think makes a lot of sense to the end user. I'd prefer something
like pg_fatal("failed to allocate a new connection").

      if (found)
-        pg_fatal("Data type checks failed: %s", report.data);
+    {
+        pg_fatal("Data type checks failed: %s", 
data_type_check_report.data);
+        termPQExpBuffer(&data_type_check_report);
+    }

`found` should be removed and replaced with `data_type_check_failed`, as
it's not set anymore. Also the termPQExpBuffer after pg_fatal looks
unnecessary.

+static bool *data_type_check_results;
+static bool data_type_check_failed;
+static PQExpBufferData data_type_check_report;

IMO, it would be nicer to have these as a local state, that's passed in
as an arg* to the AsyncTaskProcessCB, which aligns with how the other
checks do it.

-- End of review --

Regarding keeping the connections, the way I envisioned it entailed
passing a list of connections from one check to the next one (or keeping
a global state with connections?). I didn't concretely look at the code
to verify this, so it's just an abstract idea.

#21Nathan Bossart
nathandbossart@gmail.com
In reply to: Ilya Gladyshev (#20)
11 attachment(s)
Re: optimizing pg_upgrade's once-in-each-database steps

On Sun, Aug 04, 2024 at 07:19:57PM +0100, Ilya Gladyshev wrote:

-- End of review --

Thanks for the review. I've attempted to address all your feedback in v8
of the patch set. I think the names could still use some work, but I
wanted to get the main structure in place before trying to fix them.

Regarding keeping the connections, the way I envisioned it entailed passing
a list of connections from one check to the next one (or keeping a global
state with connections?). I didn't concretely look at the code to verify
this, so it's just an abstract idea.

My main concern with doing something like that is it could require
maintaining a huge number of connections when there are many databases.
GUCs like max_connections would need to be set accordingly. I'm a little
dubious that the gains would be enough to justify it.

--
nathan

Attachments:

v8-0001-introduce-framework-for-parallelizing-pg_upgrade-.patchtext/plain; charset=us-asciiDownload
From 2536a3aac4cabe70c59feb8134d88ee54214e3a2 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 11:02:44 -0500
Subject: [PATCH v8 01/11] introduce framework for parallelizing pg_upgrade
 tasks

---
 src/bin/pg_upgrade/Makefile      |   1 +
 src/bin/pg_upgrade/async.c       | 466 +++++++++++++++++++++++++++++++
 src/bin/pg_upgrade/meson.build   |   1 +
 src/bin/pg_upgrade/pg_upgrade.h  |  14 +
 src/tools/pgindent/typedefs.list |   4 +
 5 files changed, 486 insertions(+)
 create mode 100644 src/bin/pg_upgrade/async.c

diff --git a/src/bin/pg_upgrade/Makefile b/src/bin/pg_upgrade/Makefile
index bde91e2beb..3bc4f5d740 100644
--- a/src/bin/pg_upgrade/Makefile
+++ b/src/bin/pg_upgrade/Makefile
@@ -12,6 +12,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	$(WIN32RES) \
+	async.o \
 	check.o \
 	controldata.o \
 	dump.o \
diff --git a/src/bin/pg_upgrade/async.c b/src/bin/pg_upgrade/async.c
new file mode 100644
index 0000000000..e279fdd676
--- /dev/null
+++ b/src/bin/pg_upgrade/async.c
@@ -0,0 +1,466 @@
+/*
+ * async.c
+ *		parallelization via libpq's async APIs
+ *
+ * This framework provides an efficient way of running the various
+ * once-in-each-database tasks required by pg_upgrade.  Specifically, it
+ * parallelizes these tasks by managing a simple state machine of
+ * user_opts.jobs slots and using libpq's asynchronous APIs to establish the
+ * connections and run the queries.  Callers simply need to create a callback
+ * function and build/execute an AsyncTask.  A simple example follows:
+ *
+ *		static void
+ *		my_process_cb(DbInfo *dbinfo, PGresult *res, void *arg)
+ *		{
+ *			for (int i = 0; i < PQntuples(res); i++)
+ *			{
+ *				... process results ...
+ *			}
+ *		}
+ *
+ *		void
+ *		my_task(ClusterInfo *cluster)
+ *		{
+ *			AsyncTask  *task = async_task_create();
+ *
+ *			async_task_add_step(task,
+ *								"... query text ...",
+ *								my_process_cb,
+ *								true,	// let the task free the PGresult
+ *								NULL);	// "arg" pointer for the callbacks
+ *			async_task_run(task, cluster);
+ *			async_task_free(task);
+ *		}
+ *
+ * Note that multiple steps can be added to a given task.  When there are
+ * multiple steps, the task will run all of the steps consecutively in the same
+ * database connection before freeing the connection and moving on.  In other
+ * words, it only ever initiates one connection to each database in the
+ * cluster for a given run.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ * src/bin/pg_upgrade/async.c
+ */
+
+#include "postgres_fe.h"
+
+#include "common/connect.h"
+#include "fe_utils/string_utils.h"
+#include "pg_upgrade.h"
+
+/*
+ * dbs_complete stores the number of databases that we have completed
+ * processing.  When this value equals the number of databases in the cluster,
+ * the task is finished.
+ */
+static int	dbs_complete;
+
+/*
+ * dbs_processing stores the index of the next database in the cluster's array
+ * of databases that will be picked up for processing.  It will always be
+ * greater than or equal to dbs_complete.
+ */
+static int	dbs_processing;
+
+/*
+ * This struct stores all the information for a single step of a task.  All
+ * steps in a task are run in a single connection before moving on to the next
+ * database (which requires a new connection).
+ */
+typedef struct AsyncTaskCallbacks
+{
+	AsyncTaskProcessCB process_cb;	/* processes the results of the query */
+	const char *query;			/* query text */
+	bool		free_result;	/* should we free the result? */
+	void	   *arg;			/* pointer passed to each callback */
+} AsyncTaskCallbacks;
+
+/*
+ * This struct is a thin wrapper around an array of steps, i.e.,
+ * AsyncTaskCallbacks.
+ */
+typedef struct AsyncTask
+{
+	AsyncTaskCallbacks *cbs;
+	int			num_cb_sets;
+} AsyncTask;
+
+/*
+ * The different states for a parallel slot.
+ */
+typedef enum
+{
+	FREE,						/* slot available for use in a new database */
+	CONNECTING,					/* waiting for connection to be established */
+	SETTING_SEARCH_PATH,		/* waiting for search_path query to complete */
+	RUNNING_QUERY,				/* running/processing queries in the task */
+} AsyncSlotState;
+
+/*
+ * We maintain an array of user_opts.jobs slots to execute the task.
+ */
+typedef struct
+{
+	AsyncSlotState state;		/* state of the slot */
+	int			db;				/* index of the database assigned to slot */
+	int			query;			/* index of the current step in the task */
+	PGconn	   *conn;			/* current connection managed by slot */
+} AsyncSlot;
+
+/*
+ * Initializes an AsyncTask.
+ */
+AsyncTask *
+async_task_create(void)
+{
+	return pg_malloc0(sizeof(AsyncTask));
+}
+
+/*
+ * Frees all storage associated with an AsyncTask.
+ */
+void
+async_task_free(AsyncTask *task)
+{
+	if (task->cbs)
+		pg_free(task->cbs);
+
+	pg_free(task);
+}
+
+/*
+ * Adds a step to an AsyncTask.  The steps will be executed in each database in
+ * the order in which they are added.
+ *
+ *	task: task object that must have been initialized via async_task_create()
+ *	query: the query text
+ *	process_cb: function that processes the results of the query
+ *	free_result: should we free the PGresult, or leave it to the caller?
+ *	arg: pointer to task-specific data that is passed to each callback
+ */
+void
+async_task_add_step(AsyncTask *task, const char *query,
+					AsyncTaskProcessCB process_cb, bool free_result,
+					void *arg)
+{
+	AsyncTaskCallbacks *new_cbs;
+
+	task->cbs = pg_realloc(task->cbs,
+						   ++task->num_cb_sets * sizeof(AsyncTaskCallbacks));
+
+	new_cbs = &task->cbs[task->num_cb_sets - 1];
+	new_cbs->process_cb = process_cb;
+	new_cbs->query = query;
+	new_cbs->free_result = free_result;
+	new_cbs->arg = arg;
+}
+
+/*
+ * A simple wrapper around a pg_fatal() that includes the error message for the
+ * connection.
+ */
+static void
+conn_failure(PGconn *conn)
+{
+	pg_fatal("connection failure: %s", PQerrorMessage(conn));
+}
+
+/*
+ * Build a connection string for the slot's current database and asynchronously
+ * start a new connection, but do not wait for the connection to be
+ * established.
+ */
+static void
+start_conn(const ClusterInfo *cluster, AsyncSlot *slot)
+{
+	PQExpBufferData conn_opts;
+
+	/* Build connection string with proper quoting */
+	initPQExpBuffer(&conn_opts);
+	appendPQExpBufferStr(&conn_opts, "dbname=");
+	appendConnStrVal(&conn_opts, cluster->dbarr.dbs[slot->db].db_name);
+	appendPQExpBufferStr(&conn_opts, " user=");
+	appendConnStrVal(&conn_opts, os_info.user);
+	appendPQExpBuffer(&conn_opts, " port=%d", cluster->port);
+	if (cluster->sockdir)
+	{
+		appendPQExpBufferStr(&conn_opts, " host=");
+		appendConnStrVal(&conn_opts, cluster->sockdir);
+	}
+
+	slot->conn = PQconnectStart(conn_opts.data);
+
+	if (!slot->conn)
+		pg_fatal("failed to create connection with connection string: \"%s\"",
+				 conn_opts.data);
+
+	termPQExpBuffer(&conn_opts);
+}
+
+/*
+ * Start the next query, but do not wait for it to complete.
+ */
+static void
+dispatch_query(const ClusterInfo *cluster, AsyncSlot *slot,
+			   const AsyncTask *task)
+{
+	if (!PQsendQuery(slot->conn, task->cbs[slot->query].query))
+		conn_failure(slot->conn);
+}
+
+/*
+ * Cycle through all the results for a connection and return the last one.  We
+ * don't anticipate there ever being more than a single result for anything we
+ * do, so this is mostly pro forma.
+ */
+static PGresult *
+get_last_result(PGconn *conn)
+{
+	PGresult   *tmp;
+	PGresult   *res = NULL;
+
+	while ((tmp = PQgetResult(conn)) != NULL)
+	{
+		PQclear(res);
+		res = tmp;
+		if (PQstatus(conn) == CONNECTION_BAD)
+			conn_failure(conn);
+	}
+
+	if (PQresultStatus(res) != PGRES_COMMAND_OK &&
+		PQresultStatus(res) != PGRES_TUPLES_OK)
+		conn_failure(conn);
+
+	return res;
+}
+
+/*
+ * Run the process_cb callback function to process the result of a query, and
+ * free the result if the caller indicated we should do so.
+ */
+static void
+process_query_result(const ClusterInfo *cluster, AsyncSlot *slot,
+					 const AsyncTask *task)
+{
+	AsyncTaskCallbacks *cbs = &task->cbs[slot->query];
+	AsyncTaskProcessCB process_cb = cbs->process_cb;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db];
+	PGresult   *res = get_last_result(slot->conn);
+
+	(*process_cb) (dbinfo, res, cbs->arg);
+
+	if (cbs->free_result)
+		PQclear(res);
+}
+
+/*
+ * Advances the state machine for a given slot as necessary.
+ */
+static void
+process_slot(const ClusterInfo *cluster, AsyncSlot *slot, const AsyncTask *task)
+{
+	switch (slot->state)
+	{
+		case FREE:
+
+			/*
+			 * If all of the databases in the cluster have been processed or
+			 * are currently being processed by other slots, we are done.
+			 */
+			if (dbs_processing >= cluster->dbarr.ndbs)
+				return;
+
+			/*
+			 * Claim the next database in the cluster's array and initiate a
+			 * new connection.
+			 */
+			slot->db = dbs_processing++;
+			slot->state = CONNECTING;
+			start_conn(cluster, slot);
+
+			return;
+
+		case CONNECTING:
+
+			/* Check for connection failure. */
+			if (PQconnectPoll(slot->conn) == PGRES_POLLING_FAILED)
+				conn_failure(slot->conn);
+
+			/* Check whether the connection is still establishing. */
+			if (PQconnectPoll(slot->conn) != PGRES_POLLING_OK)
+				return;
+
+			/*
+			 * Move on to setting the search_path for the connection to a
+			 * known-safe value.  This is common for all tasks/steps and
+			 * should always be done first.
+			 */
+			slot->state = SETTING_SEARCH_PATH;
+			if (!PQsendQuery(slot->conn, ALWAYS_SECURE_SEARCH_PATH_SQL))
+				conn_failure(slot->conn);
+
+			return;
+
+		case SETTING_SEARCH_PATH:
+
+			/* Check whether the query is still in progress. */
+			if (!PQconsumeInput(slot->conn))
+				conn_failure(slot->conn);
+			if (PQisBusy(slot->conn))
+				return;
+
+			/* Discard the result of the search_path query. */
+			PQclear(get_last_result(slot->conn));
+
+			/* Start running the query for the first step in the task. */
+			slot->state = RUNNING_QUERY;
+			dispatch_query(cluster, slot, task);
+
+			return;
+
+		case RUNNING_QUERY:
+
+			/* Check whether the query is still in progress. */
+			if (!PQconsumeInput(slot->conn))
+				conn_failure(slot->conn);
+			if (PQisBusy(slot->conn))
+				return;
+
+			/* Process the query result. */
+			process_query_result(cluster, slot, task);
+
+			/*
+			 * If we just finished processing the result of the last step in
+			 * the task, free the slot.  We recursively call this function on
+			 * the newly-freed slot so that we can start initiating the next
+			 * connection immediately instead of waiting for the next loop
+			 * through the slots.
+			 */
+			if (++slot->query >= task->num_cb_sets)
+			{
+				dbs_complete++;
+				PQfinish(slot->conn);
+				memset(slot, 0, sizeof(AsyncSlot));
+
+				process_slot(cluster, slot, task);
+
+				return;
+			}
+
+			/* Start running the query for the next step in the task. */
+			dispatch_query(cluster, slot, task);
+			return;
+	}
+}
+
+/*
+ * Wait on the slots to either finish connecting or to receive query results if
+ * possible.  This avoids a tight loop in async_task_run().
+ */
+static void
+wait_on_slots(AsyncSlot *slots, int numslots)
+{
+	fd_set		input_mask;
+	fd_set		output_mask;
+	fd_set		except_mask;
+	int			maxFd = 0;
+
+	FD_ZERO(&input_mask);
+	FD_ZERO(&output_mask);
+	FD_ZERO(&except_mask);
+
+	for (int i = 0; i < numslots; i++)
+	{
+		int			sock;
+		bool		read = false;
+
+		switch (slots[i].state)
+		{
+			case FREE:
+
+				/*
+				 * This function should only ever see free slots as we are
+				 * finishing processing the last few databases, at which point
+				 * we don't have any databases left for them to process. We'll
+				 * never use these slots again, so we can safely ignore them.
+				 */
+				continue;
+
+			case CONNECTING:
+
+				/*
+				 * If we are waiting for the connection to establish, choose
+				 * whether to wait for reading or for writing on the socket as
+				 * appropriate.  If neither apply, just return immediately so
+				 * that we can handle the slot.
+				 */
+				{
+					PostgresPollingStatusType status;
+
+					status = PQconnectPoll(slots[i].conn);
+					if (status == PGRES_POLLING_READING)
+						read = true;
+					else if (status != PGRES_POLLING_WRITING)
+						return;
+				}
+				break;
+
+			case SETTING_SEARCH_PATH:
+			case RUNNING_QUERY:
+
+				/*
+				 * If we've sent a query, we must wait for the socket to be
+				 * read-ready.  Note that process_slot() handles calling
+				 * PQconsumeInput() as required.
+				 */
+				read = true;
+				break;
+		}
+
+		/*
+		 * If there's some problem retrieving the socket, just pretend this
+		 * slot doesn't exist.  We don't expect this to happen regularly in
+		 * practice, so it seems unlikely to cause too much harm.
+		 */
+		sock = PQsocket(slots[i].conn);
+		if (sock < 0)
+			continue;
+
+		/*
+		 * Add the socket to the set.
+		 */
+		FD_SET(sock, read ? &input_mask : &output_mask);
+		FD_SET(sock, &except_mask);
+		maxFd = Max(maxFd, sock);
+	}
+
+	/*
+	 * If we found socket(s) to wait on, wait.
+	 */
+	if (maxFd != 0)
+		(void) select(maxFd + 1, &input_mask, &output_mask, &except_mask, NULL);
+}
+
+/*
+ * Runs all the steps of the task in every database in the cluster using
+ * user_opts.jobs parallel slots.
+ */
+void
+async_task_run(const AsyncTask *task, const ClusterInfo *cluster)
+{
+	int			jobs = Max(1, user_opts.jobs);
+	AsyncSlot  *slots = pg_malloc0(sizeof(AsyncSlot) * jobs);
+
+	dbs_complete = 0;
+	dbs_processing = 0;
+
+	while (dbs_complete < cluster->dbarr.ndbs)
+	{
+		for (int i = 0; i < jobs; i++)
+			process_slot(cluster, &slots[i], task);
+
+		wait_on_slots(slots, jobs);
+	}
+
+	pg_free(slots);
+}
diff --git a/src/bin/pg_upgrade/meson.build b/src/bin/pg_upgrade/meson.build
index 9825fa3305..9eb48e176c 100644
--- a/src/bin/pg_upgrade/meson.build
+++ b/src/bin/pg_upgrade/meson.build
@@ -1,6 +1,7 @@
 # Copyright (c) 2022-2024, PostgreSQL Global Development Group
 
 pg_upgrade_sources = files(
+  'async.c',
   'check.c',
   'controldata.c',
   'dump.c',
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index cdb6e2b759..5f319af34d 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -494,3 +494,17 @@ void		parallel_transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr
 										  char *old_pgdata, char *new_pgdata,
 										  char *old_tablespace);
 bool		reap_child(bool wait_for_child);
+
+/* async.c */
+
+typedef void (*AsyncTaskProcessCB) (DbInfo *dbinfo, PGresult *res, void *arg);
+
+/* struct definition is private to async.c */
+typedef struct AsyncTask AsyncTask;
+
+AsyncTask  *async_task_create(void);
+void		async_task_add_step(AsyncTask *task, const char *query,
+								AsyncTaskProcessCB process_cb, bool free_result,
+								void *arg);
+void		async_task_run(const AsyncTask *task, const ClusterInfo *cluster);
+void		async_task_free(AsyncTask *task);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 547d14b3e7..ec8106329d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -153,6 +153,10 @@ ArrayMetaState
 ArraySubWorkspace
 ArrayToken
 ArrayType
+AsyncSlot
+AsyncSlotState
+AsyncTask
+AsyncTaskCallbacks
 AsyncQueueControl
 AsyncQueueEntry
 AsyncRequest
-- 
2.39.3 (Apple Git-146)

v8-0002-use-new-pg_upgrade-async-API-for-subscription-sta.patchtext/plain; charset=us-asciiDownload
From c91af07418d9824d6416380a8496088aad502b18 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 17:21:19 -0500
Subject: [PATCH v8 02/11] use new pg_upgrade async API for subscription state
 checks

---
 src/bin/pg_upgrade/check.c | 204 ++++++++++++++++++++-----------------
 1 file changed, 110 insertions(+), 94 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 5038231731..48f5a0c0e6 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1905,6 +1905,79 @@ check_old_cluster_for_valid_slots(void)
 	check_ok();
 }
 
+/* private state for subscription state checks */
+struct substate_info
+{
+	FILE	   *script;
+	char		output_path[MAXPGPATH];
+};
+
+/*
+ * We don't allow upgrade if there is a risk of dangling slot or origin
+ * corresponding to initial sync after upgrade.
+ *
+ * A slot/origin not created yet refers to the 'i' (initialize) state, while
+ * 'r' (ready) state refers to a slot/origin created previously but already
+ * dropped. These states are supported for pg_upgrade. The other states listed
+ * below are not supported:
+ *
+ * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state would
+ * retain a replication slot, which could not be dropped by the sync worker
+ * spawned after the upgrade because the subscription ID used for the slot name
+ * won't match anymore.
+ *
+ * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state would
+ * retain the replication origin when there is a failure in tablesync worker
+ * immediately after dropping the replication slot in the publisher.
+ *
+ * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on a
+ * relation upgraded while in this state would expect an origin ID with the OID
+ * of the subscription used before the upgrade, causing it to fail.
+ *
+ * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and SUBREL_STATE_UNKNOWN:
+ * These states are not stored in the catalog, so we need not allow these
+ * states.
+ */
+static const char *
+sub_query(void)
+{
+	return "SELECT r.srsubstate, s.subname, n.nspname, c.relname "
+		"FROM pg_catalog.pg_subscription_rel r "
+		"LEFT JOIN pg_catalog.pg_subscription s"
+		"   ON r.srsubid = s.oid "
+		"LEFT JOIN pg_catalog.pg_class c"
+		"   ON r.srrelid = c.oid "
+		"LEFT JOIN pg_catalog.pg_namespace n"
+		"   ON c.relnamespace = n.oid "
+		"WHERE r.srsubstate NOT IN ('i', 'r') "
+		"ORDER BY s.subname";
+}
+
+static void
+sub_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct substate_info *state = (struct substate_info *) arg;
+	int			ntup = PQntuples(res);
+	int			i_srsubstate = PQfnumber(res, "srsubstate");
+	int			i_subname = PQfnumber(res, "subname");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+
+	for (int i = 0; i < ntup; i++)
+	{
+		if (state->script == NULL &&
+			(state->script = fopen_priv(state->output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state->output_path);
+
+		fprintf(state->script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
+				PQgetvalue(res, i, i_srsubstate),
+				dbinfo->db_name,
+				PQgetvalue(res, i, i_subname),
+				PQgetvalue(res, i, i_nspname),
+				PQgetvalue(res, i, i_relname));
+	}
+}
+
 /*
  * check_old_cluster_subscription_state()
  *
@@ -1915,115 +1988,58 @@ check_old_cluster_for_valid_slots(void)
 static void
 check_old_cluster_subscription_state(void)
 {
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
+	struct substate_info state;
+	PGresult   *res;
+	PGconn	   *conn;
 	int			ntup;
 
 	prep_status("Checking for subscription state");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	state.script = NULL;
+	snprintf(state.output_path, sizeof(state.output_path), "%s/%s",
 			 log_opts.basedir,
 			 "subs_invalid.txt");
-	for (int dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
 
-		/* We need to check for pg_replication_origin only once. */
-		if (dbnum == 0)
-		{
-			/*
-			 * Check that all the subscriptions have their respective
-			 * replication origin.
-			 */
-			res = executeQueryOrDie(conn,
-									"SELECT d.datname, s.subname "
-									"FROM pg_catalog.pg_subscription s "
-									"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
-									"	ON o.roname = 'pg_' || s.oid "
-									"INNER JOIN pg_catalog.pg_database d "
-									"	ON d.oid = s.subdbid "
-									"WHERE o.roname IS NULL;");
-
-			ntup = PQntuples(res);
-			for (int i = 0; i < ntup; i++)
-			{
-				if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-					pg_fatal("could not open file \"%s\": %m", output_path);
-				fprintf(script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
-						PQgetvalue(res, i, 0),
-						PQgetvalue(res, i, 1));
-			}
-			PQclear(res);
-		}
-
-		/*
-		 * We don't allow upgrade if there is a risk of dangling slot or
-		 * origin corresponding to initial sync after upgrade.
-		 *
-		 * A slot/origin not created yet refers to the 'i' (initialize) state,
-		 * while 'r' (ready) state refers to a slot/origin created previously
-		 * but already dropped. These states are supported for pg_upgrade. The
-		 * other states listed below are not supported:
-		 *
-		 * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state
-		 * would retain a replication slot, which could not be dropped by the
-		 * sync worker spawned after the upgrade because the subscription ID
-		 * used for the slot name won't match anymore.
-		 *
-		 * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state
-		 * would retain the replication origin when there is a failure in
-		 * tablesync worker immediately after dropping the replication slot in
-		 * the publisher.
-		 *
-		 * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on
-		 * a relation upgraded while in this state would expect an origin ID
-		 * with the OID of the subscription used before the upgrade, causing
-		 * it to fail.
-		 *
-		 * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and
-		 * SUBREL_STATE_UNKNOWN: These states are not stored in the catalog,
-		 * so we need not allow these states.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT r.srsubstate, s.subname, n.nspname, c.relname "
-								"FROM pg_catalog.pg_subscription_rel r "
-								"LEFT JOIN pg_catalog.pg_subscription s"
-								"	ON r.srsubid = s.oid "
-								"LEFT JOIN pg_catalog.pg_class c"
-								"	ON r.srrelid = c.oid "
-								"LEFT JOIN pg_catalog.pg_namespace n"
-								"	ON c.relnamespace = n.oid "
-								"WHERE r.srsubstate NOT IN ('i', 'r') "
-								"ORDER BY s.subname");
-
-		ntup = PQntuples(res);
-		for (int i = 0; i < ntup; i++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
+	/*
+	 * Check that all the subscriptions have their respective replication
+	 * origin.  This check only needs to run once.
+	 */
+	conn = connectToServer(&old_cluster, old_cluster.dbarr.dbs[0].db_name);
+	res = executeQueryOrDie(conn,
+							"SELECT d.datname, s.subname "
+							"FROM pg_catalog.pg_subscription s "
+							"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
+							"	ON o.roname = 'pg_' || s.oid "
+							"INNER JOIN pg_catalog.pg_database d "
+							"	ON d.oid = s.subdbid "
+							"WHERE o.roname IS NULL;");
+	ntup = PQntuples(res);
+	for (int i = 0; i < ntup; i++)
+	{
+		if (state.script == NULL &&
+			(state.script = fopen_priv(state.output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state.output_path);
+		fprintf(state.script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
+				PQgetvalue(res, i, 0),
+				PQgetvalue(res, i, 1));
+	}
+	PQclear(res);
+	PQfinish(conn);
 
-			fprintf(script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
-					PQgetvalue(res, i, 0),
-					active_db->db_name,
-					PQgetvalue(res, i, 1),
-					PQgetvalue(res, i, 2),
-					PQgetvalue(res, i, 3));
-		}
+	async_task_add_step(task, sub_query(), sub_process, true, &state);
 
-		PQclear(res);
-		PQfinish(conn);
-	}
+	async_task_run(task, &old_cluster);
+	async_task_free(task);
 
-	if (script)
+	if (state.script)
 	{
-		fclose(script);
+		fclose(state.script);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains subscriptions without origin or having relations not in i (initialize) or r (ready) state.\n"
 				 "You can allow the initial sync to finish for all relations and then restart the upgrade.\n"
 				 "A list of the problematic subscriptions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", state.output_path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v8-0003-use-new-pg_upgrade-async-API-for-retrieving-relin.patchtext/plain; charset=us-asciiDownload
From 93d3ce52ae0d9cf37777a54a837708ad3293b9f3 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:09:33 -0500
Subject: [PATCH v8 03/11] use new pg_upgrade async API for retrieving relinfos

---
 src/bin/pg_upgrade/info.c | 246 +++++++++++++++++++-------------------
 1 file changed, 120 insertions(+), 126 deletions(-)

diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 5de5e10945..5d93e9716d 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -11,6 +11,7 @@
 
 #include "access/transam.h"
 #include "catalog/pg_class_d.h"
+#include "pqexpbuffer.h"
 #include "pg_upgrade.h"
 
 static void create_rel_filename_map(const char *old_data, const char *new_data,
@@ -22,12 +23,14 @@ static void report_unmatched_relation(const RelInfo *rel, const DbInfo *db,
 static void free_db_and_rel_infos(DbInfoArr *db_arr);
 static void get_template0_info(ClusterInfo *cluster);
 static void get_db_infos(ClusterInfo *cluster);
-static void get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo);
+static char *get_rel_infos_query(void);
+static void get_rel_infos_result(DbInfo *dbinfo, PGresult *res, void *arg);
 static void free_rel_infos(RelInfoArr *rel_arr);
 static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
-static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo);
+static char *get_old_cluster_logical_slot_infos_query(void);
+static void get_old_cluster_logical_slot_infos_result(DbInfo *dbinfo, PGresult *res, void *arg);
 
 
 /*
@@ -276,7 +279,9 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
 void
 get_db_rel_and_slot_infos(ClusterInfo *cluster)
 {
-	int			dbnum;
+	AsyncTask  *task = async_task_create();
+	char	   *rel_infos_query = NULL;
+	char	   *logical_slot_infos_query = NULL;
 
 	if (cluster->dbarr.dbs != NULL)
 		free_db_and_rel_infos(&cluster->dbarr);
@@ -284,15 +289,29 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster)
 	get_template0_info(cluster);
 	get_db_infos(cluster);
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	rel_infos_query = get_rel_infos_query();
+	async_task_add_step(task,
+						rel_infos_query,
+						get_rel_infos_result,
+						true, NULL);
+
+	if (cluster == &old_cluster &&
+		GET_MAJOR_VERSION(cluster->major_version) > 1600)
 	{
-		DbInfo	   *pDbInfo = &cluster->dbarr.dbs[dbnum];
+		logical_slot_infos_query = get_old_cluster_logical_slot_infos_query();
+		async_task_add_step(task,
+							logical_slot_infos_query,
+							get_old_cluster_logical_slot_infos_result,
+							true, NULL);
+	}
 
-		get_rel_infos(cluster, pDbInfo);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
-		if (cluster == &old_cluster)
-			get_old_cluster_logical_slot_infos(pDbInfo);
-	}
+	if (rel_infos_query)
+		pg_free(rel_infos_query);
+	if (logical_slot_infos_query)
+		pg_free(logical_slot_infos_query);
 
 	if (cluster == &old_cluster)
 		pg_log(PG_VERBOSE, "\nsource databases:");
@@ -439,32 +458,12 @@ get_db_infos(ClusterInfo *cluster)
  * Note: the resulting RelInfo array is assumed to be sorted by OID.
  * This allows later processing to match up old and new databases efficiently.
  */
-static void
-get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
+static char *
+get_rel_infos_query(void)
 {
-	PGconn	   *conn = connectToServer(cluster,
-									   dbinfo->db_name);
-	PGresult   *res;
-	RelInfo    *relinfos;
-	int			ntups;
-	int			relnum;
-	int			num_rels = 0;
-	char	   *nspname = NULL;
-	char	   *relname = NULL;
-	char	   *tablespace = NULL;
-	int			i_spclocation,
-				i_nspname,
-				i_relname,
-				i_reloid,
-				i_indtable,
-				i_toastheap,
-				i_relfilenumber,
-				i_reltablespace;
-	char		query[QUERY_ALLOC];
-	char	   *last_namespace = NULL,
-			   *last_tablespace = NULL;
+	PQExpBufferData query;
 
-	query[0] = '\0';			/* initialize query string to empty */
+	initPQExpBuffer(&query);
 
 	/*
 	 * Create a CTE that collects OIDs of regular user tables and matviews,
@@ -476,34 +475,34 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * output, so we have to copy that system table.  It's easiest to do that
 	 * by treating it as a user table.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "WITH regular_heap (reloid, indtable, toastheap) AS ( "
-			 "  SELECT c.oid, 0::oid, 0::oid "
-			 "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
-			 "         ON c.relnamespace = n.oid "
-			 "  WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
-			 CppAsString2(RELKIND_MATVIEW) ") AND "
+	appendPQExpBuffer(&query,
+					  "WITH regular_heap (reloid, indtable, toastheap) AS ( "
+					  "  SELECT c.oid, 0::oid, 0::oid "
+					  "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
+					  "         ON c.relnamespace = n.oid "
+					  "  WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
+					  CppAsString2(RELKIND_MATVIEW) ") AND "
 	/* exclude possible orphaned temp tables */
-			 "    ((n.nspname !~ '^pg_temp_' AND "
-			 "      n.nspname !~ '^pg_toast_temp_' AND "
-			 "      n.nspname NOT IN ('pg_catalog', 'information_schema', "
-			 "                        'binary_upgrade', 'pg_toast') AND "
-			 "      c.oid >= %u::pg_catalog.oid) OR "
-			 "     (n.nspname = 'pg_catalog' AND "
-			 "      relname IN ('pg_largeobject') ))), ",
-			 FirstNormalObjectId);
+					  "    ((n.nspname !~ '^pg_temp_' AND "
+					  "      n.nspname !~ '^pg_toast_temp_' AND "
+					  "      n.nspname NOT IN ('pg_catalog', 'information_schema', "
+					  "                        'binary_upgrade', 'pg_toast') AND "
+					  "      c.oid >= %u::pg_catalog.oid) OR "
+					  "     (n.nspname = 'pg_catalog' AND "
+					  "      relname IN ('pg_largeobject') ))), ",
+					  FirstNormalObjectId);
 
 	/*
 	 * Add a CTE that collects OIDs of toast tables belonging to the tables
 	 * selected by the regular_heap CTE.  (We have to do this separately
 	 * because the namespace-name rules above don't work for toast tables.)
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "  toast_heap (reloid, indtable, toastheap) AS ( "
-			 "  SELECT c.reltoastrelid, 0::oid, c.oid "
-			 "  FROM regular_heap JOIN pg_catalog.pg_class c "
-			 "      ON regular_heap.reloid = c.oid "
-			 "  WHERE c.reltoastrelid != 0), ");
+	appendPQExpBufferStr(&query,
+						 "  toast_heap (reloid, indtable, toastheap) AS ( "
+						 "  SELECT c.reltoastrelid, 0::oid, c.oid "
+						 "  FROM regular_heap JOIN pg_catalog.pg_class c "
+						 "      ON regular_heap.reloid = c.oid "
+						 "  WHERE c.reltoastrelid != 0), ");
 
 	/*
 	 * Add a CTE that collects OIDs of all valid indexes on the previously
@@ -511,53 +510,61 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * Testing indisready is necessary in 9.2, and harmless in earlier/later
 	 * versions.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "  all_index (reloid, indtable, toastheap) AS ( "
-			 "  SELECT indexrelid, indrelid, 0::oid "
-			 "  FROM pg_catalog.pg_index "
-			 "  WHERE indisvalid AND indisready "
-			 "    AND indrelid IN "
-			 "        (SELECT reloid FROM regular_heap "
-			 "         UNION ALL "
-			 "         SELECT reloid FROM toast_heap)) ");
+	appendPQExpBufferStr(&query,
+						 "  all_index (reloid, indtable, toastheap) AS ( "
+						 "  SELECT indexrelid, indrelid, 0::oid "
+						 "  FROM pg_catalog.pg_index "
+						 "  WHERE indisvalid AND indisready "
+						 "    AND indrelid IN "
+						 "        (SELECT reloid FROM regular_heap "
+						 "         UNION ALL "
+						 "         SELECT reloid FROM toast_heap)) ");
 
 	/*
 	 * And now we can write the query that retrieves the data we want for each
 	 * heap and index relation.  Make sure result is sorted by OID.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "SELECT all_rels.*, n.nspname, c.relname, "
-			 "  c.relfilenode, c.reltablespace, "
-			 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
-			 "FROM (SELECT * FROM regular_heap "
-			 "      UNION ALL "
-			 "      SELECT * FROM toast_heap "
-			 "      UNION ALL "
-			 "      SELECT * FROM all_index) all_rels "
-			 "  JOIN pg_catalog.pg_class c "
-			 "      ON all_rels.reloid = c.oid "
-			 "  JOIN pg_catalog.pg_namespace n "
-			 "     ON c.relnamespace = n.oid "
-			 "  LEFT OUTER JOIN pg_catalog.pg_tablespace t "
-			 "     ON c.reltablespace = t.oid "
-			 "ORDER BY 1;");
-
-	res = executeQueryOrDie(conn, "%s", query);
-
-	ntups = PQntuples(res);
-
-	relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	appendPQExpBufferStr(&query,
+						 "SELECT all_rels.*, n.nspname, c.relname, "
+						 "  c.relfilenode, c.reltablespace, "
+						 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
+						 "FROM (SELECT * FROM regular_heap "
+						 "      UNION ALL "
+						 "      SELECT * FROM toast_heap "
+						 "      UNION ALL "
+						 "      SELECT * FROM all_index) all_rels "
+						 "  JOIN pg_catalog.pg_class c "
+						 "      ON all_rels.reloid = c.oid "
+						 "  JOIN pg_catalog.pg_namespace n "
+						 "     ON c.relnamespace = n.oid "
+						 "  LEFT OUTER JOIN pg_catalog.pg_tablespace t "
+						 "     ON c.reltablespace = t.oid "
+						 "ORDER BY 1;");
+
+	return query.data;
+}
 
-	i_reloid = PQfnumber(res, "reloid");
-	i_indtable = PQfnumber(res, "indtable");
-	i_toastheap = PQfnumber(res, "toastheap");
-	i_nspname = PQfnumber(res, "nspname");
-	i_relname = PQfnumber(res, "relname");
-	i_relfilenumber = PQfnumber(res, "relfilenode");
-	i_reltablespace = PQfnumber(res, "reltablespace");
-	i_spclocation = PQfnumber(res, "spclocation");
+static void
+get_rel_infos_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	int			ntups = PQntuples(res);
+	RelInfo    *relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	int			i_reloid = PQfnumber(res, "reloid");
+	int			i_indtable = PQfnumber(res, "indtable");
+	int			i_toastheap = PQfnumber(res, "toastheap");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+	int			i_relfilenumber = PQfnumber(res, "relfilenode");
+	int			i_reltablespace = PQfnumber(res, "reltablespace");
+	int			i_spclocation = PQfnumber(res, "spclocation");
+	int			num_rels = 0;
+	char	   *nspname = NULL;
+	char	   *relname = NULL;
+	char	   *tablespace = NULL;
+	char	   *last_namespace = NULL;
+	char	   *last_tablespace = NULL;
 
-	for (relnum = 0; relnum < ntups; relnum++)
+	for (int relnum = 0; relnum < ntups; relnum++)
 	{
 		RelInfo    *curr = &relinfos[num_rels++];
 
@@ -610,9 +617,6 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 			/* A zero reltablespace oid indicates the database tablespace. */
 			curr->tablespace = dbinfo->db_tablespace;
 	}
-	PQclear(res);
-
-	PQfinish(conn);
 
 	dbinfo->rel_arr.rels = relinfos;
 	dbinfo->rel_arr.nrels = num_rels;
@@ -634,20 +638,9 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
  * check_old_cluster_for_valid_slots() would raise a FATAL error if such slots
  * are included.
  */
-static void
-get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
+static char *
+get_old_cluster_logical_slot_infos_query(void)
 {
-	PGconn	   *conn;
-	PGresult   *res;
-	LogicalSlotInfo *slotinfos = NULL;
-	int			num_slots;
-
-	/* Logical slots can be migrated since PG17. */
-	if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1600)
-		return;
-
-	conn = connectToServer(&old_cluster, dbinfo->db_name);
-
 	/*
 	 * Fetch the logical replication slot information. The check whether the
 	 * slot is considered caught up is done by an upgrade function. This
@@ -665,18 +658,23 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
-							"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
-							"FROM pg_catalog.pg_replication_slots "
-							"WHERE slot_type = 'logical' AND "
-							"database = current_database() AND "
-							"temporary IS FALSE;",
-							user_opts.live_check ? "FALSE" :
-							"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
-							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
-							"END)");
-
-	num_slots = PQntuples(res);
+	return psprintf("SELECT slot_name, plugin, two_phase, failover, "
+					"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
+					"FROM pg_catalog.pg_replication_slots "
+					"WHERE slot_type = 'logical' AND "
+					"database = current_database() AND "
+					"temporary IS FALSE;",
+					user_opts.live_check ? "FALSE" :
+					"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
+					"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
+					"END)");
+}
+
+static void
+get_old_cluster_logical_slot_infos_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	LogicalSlotInfo *slotinfos = NULL;
+	int			num_slots = PQntuples(res);
 
 	if (num_slots)
 	{
@@ -709,14 +707,10 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 		}
 	}
 
-	PQclear(res);
-	PQfinish(conn);
-
 	dbinfo->slot_arr.slots = slotinfos;
 	dbinfo->slot_arr.nslots = num_slots;
 }
 
-
 /*
  * count_old_cluster_logical_slots()
  *
-- 
2.39.3 (Apple Git-146)

v8-0004-use-new-pg_upgrade-async-API-to-parallelize-getti.patchtext/plain; charset=us-asciiDownload
From d696ef52ff8ac8bbbe65e4ea6e5554606cbca19d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:24:35 -0500
Subject: [PATCH v8 04/11] use new pg_upgrade async API to parallelize getting
 loadable libraries

---
 src/bin/pg_upgrade/function.c | 49 +++++++++++++++++++++--------------
 1 file changed, 29 insertions(+), 20 deletions(-)

diff --git a/src/bin/pg_upgrade/function.c b/src/bin/pg_upgrade/function.c
index 7e3abed098..75e5ebb2c8 100644
--- a/src/bin/pg_upgrade/function.c
+++ b/src/bin/pg_upgrade/function.c
@@ -42,6 +42,20 @@ library_name_compare(const void *p1, const void *p2)
 					  ((const LibraryInfo *) p2)->dbnum);
 }
 
+struct loadable_libraries_state
+{
+	PGresult  **ress;
+	int			totaltups;
+};
+
+static void
+get_loadable_libraries_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct loadable_libraries_state *state = (struct loadable_libraries_state *) arg;
+
+	state->ress[dbinfo - old_cluster.dbarr.dbs] = res;
+	state->totaltups += PQntuples(res);
+}
 
 /*
  * get_loadable_libraries()
@@ -54,47 +68,41 @@ library_name_compare(const void *p1, const void *p2)
 void
 get_loadable_libraries(void)
 {
-	PGresult  **ress;
 	int			totaltups;
 	int			dbnum;
 	int			n_libinfos;
+	AsyncTask  *task = async_task_create();
+	struct loadable_libraries_state state;
+	char	   *loadable_libraries_query;
 
-	ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
-	totaltups = 0;
-
-	/* Fetch all library names, removing duplicates within each DB */
-	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
+	state.ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
+	state.totaltups = 0;
 
-		/*
-		 * Fetch all libraries containing non-built-in C functions in this DB.
-		 */
-		ress[dbnum] = executeQueryOrDie(conn,
-										"SELECT DISTINCT probin "
+	loadable_libraries_query = psprintf("SELECT DISTINCT probin "
 										"FROM pg_catalog.pg_proc "
 										"WHERE prolang = %u AND "
 										"probin IS NOT NULL AND "
 										"oid >= %u;",
 										ClanguageId,
 										FirstNormalObjectId);
-		totaltups += PQntuples(ress[dbnum]);
 
-		PQfinish(conn);
-	}
+	async_task_add_step(task, loadable_libraries_query,
+						get_loadable_libraries_result, false, &state);
+
+	async_task_run(task, &old_cluster);
+	async_task_free(task);
 
 	/*
 	 * Allocate memory for required libraries and logical replication output
 	 * plugins.
 	 */
-	n_libinfos = totaltups + count_old_cluster_logical_slots();
+	n_libinfos = state.totaltups + count_old_cluster_logical_slots();
 	os_info.libraries = (LibraryInfo *) pg_malloc(sizeof(LibraryInfo) * n_libinfos);
 	totaltups = 0;
 
 	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
 	{
-		PGresult   *res = ress[dbnum];
+		PGresult   *res = state.ress[dbnum];
 		int			ntups;
 		int			rowno;
 		LogicalSlotInfoArr *slot_arr = &old_cluster.dbarr.dbs[dbnum].slot_arr;
@@ -129,7 +137,8 @@ get_loadable_libraries(void)
 		}
 	}
 
-	pg_free(ress);
+	pg_free(state.ress);
+	pg_free(loadable_libraries_query);
 
 	os_info.num_libraries = totaltups;
 }
-- 
2.39.3 (Apple Git-146)

v8-0005-use-new-pg_upgrade-async-API-to-parallelize-repor.patchtext/plain; charset=us-asciiDownload
From d7df4159a099aa3a7ea86e0496c93d764967d52d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:31:57 -0500
Subject: [PATCH v8 05/11] use new pg_upgrade async API to parallelize
 reporting extension updates

---
 src/bin/pg_upgrade/version.c | 77 +++++++++++++++++-------------------
 1 file changed, 36 insertions(+), 41 deletions(-)

diff --git a/src/bin/pg_upgrade/version.c b/src/bin/pg_upgrade/version.c
index 2de6dffccd..dd2b0cd975 100644
--- a/src/bin/pg_upgrade/version.c
+++ b/src/bin/pg_upgrade/version.c
@@ -139,6 +139,34 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 		check_ok();
 }
 
+static void
+report_extension_updates_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_name = PQfnumber(res, "name");
+	char	   *output_path = "update_extensions.sql";
+	FILE	  **script = (FILE **) arg;
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL && (*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			PQExpBufferData connectbuf;
+
+			initPQExpBuffer(&connectbuf);
+			appendPsqlMetaConnect(&connectbuf, dbinfo->db_name);
+			fputs(connectbuf.data, *script);
+			termPQExpBuffer(&connectbuf);
+			db_used = true;
+		}
+		fprintf(*script, "ALTER EXTENSION %s UPDATE;\n",
+				quote_identifier(PQgetvalue(res, rowno, i_name)));
+	}
+}
+
 /*
  * report_extension_updates()
  *	Report extensions that should be updated.
@@ -146,53 +174,20 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 void
 report_extension_updates(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char	   *output_path = "update_extensions.sql";
+	AsyncTask  *task = async_task_create();
+	const char *query = "SELECT name "
+		"FROM pg_available_extensions "
+		"WHERE installed_version != default_version";
 
 	prep_status("Checking for extension updates");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_name;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* find extensions needing updates */
-		res = executeQueryOrDie(conn,
-								"SELECT name "
-								"FROM pg_available_extensions "
-								"WHERE installed_version != default_version"
-			);
+	async_task_add_step(task, query, report_extension_updates_result,
+						true, &script);
 
-		ntups = PQntuples(res);
-		i_name = PQfnumber(res, "name");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				PQExpBufferData connectbuf;
-
-				initPQExpBuffer(&connectbuf);
-				appendPsqlMetaConnect(&connectbuf, active_db->db_name);
-				fputs(connectbuf.data, script);
-				termPQExpBuffer(&connectbuf);
-				db_used = true;
-			}
-			fprintf(script, "ALTER EXTENSION %s UPDATE;\n",
-					quote_identifier(PQgetvalue(res, rowno, i_name)));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v8-0006-parallelize-data-type-checks-in-pg_upgrade.patchtext/plain; charset=us-asciiDownload
From cbb688d9a1849924acb1fe35a0e59e957617a044 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Sat, 6 Jul 2024 21:06:31 -0500
Subject: [PATCH v8 06/11] parallelize data type checks in pg_upgrade

---
 src/bin/pg_upgrade/check.c | 333 +++++++++++++++++++------------------
 1 file changed, 173 insertions(+), 160 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 48f5a0c0e6..150c101d1c 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -314,6 +314,133 @@ static DataTypesUsageChecks data_types_usage_checks[] =
 	}
 };
 
+struct data_type_check_state
+{
+	DataTypesUsageChecks *check;
+	bool		result;
+	PQExpBuffer *report;
+};
+
+static char *
+data_type_check_query(int checknum)
+{
+	DataTypesUsageChecks *check = &data_types_usage_checks[checknum];
+
+	return psprintf("WITH RECURSIVE oids AS ( "
+	/* start with the type(s) returned by base_query */
+					"	%s "
+					"	UNION ALL "
+					"	SELECT * FROM ( "
+	/* inner WITH because we can only reference the CTE once */
+					"		WITH x AS (SELECT oid FROM oids) "
+	/* domains on any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
+					"			UNION ALL "
+	/* arrays over any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
+					"			UNION ALL "
+	/* composite types containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
+					"			WHERE t.typtype = 'c' AND "
+					"				  t.oid = c.reltype AND "
+					"				  c.oid = a.attrelid AND "
+					"				  NOT a.attisdropped AND "
+					"				  a.atttypid = x.oid "
+					"			UNION ALL "
+	/* ranges containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
+					"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
+					"	) foo "
+					") "
+	/* now look for stored columns of any such type */
+					"SELECT n.nspname, c.relname, a.attname "
+					"FROM	pg_catalog.pg_class c, "
+					"		pg_catalog.pg_namespace n, "
+					"		pg_catalog.pg_attribute a "
+					"WHERE	c.oid = a.attrelid AND "
+					"		NOT a.attisdropped AND "
+					"		a.atttypid IN (SELECT oid FROM oids) AND "
+					"		c.relkind IN ("
+					CppAsString2(RELKIND_RELATION) ", "
+					CppAsString2(RELKIND_MATVIEW) ", "
+					CppAsString2(RELKIND_INDEX) ") AND "
+					"		c.relnamespace = n.oid AND "
+	/* exclude possible orphaned temp tables */
+					"		n.nspname !~ '^pg_temp_' AND "
+					"		n.nspname !~ '^pg_toast_temp_' AND "
+	/* exclude system catalogs, too */
+					"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
+					check->base_query);
+}
+
+static void
+data_type_check_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct data_type_check_state *state = (struct data_type_check_state *) arg;
+	int			ntups = PQntuples(res);
+
+	if (ntups)
+	{
+		char		output_path[MAXPGPATH];
+		int			i_nspname;
+		int			i_relname;
+		int			i_attname;
+		FILE	   *script = NULL;
+		bool		db_used = false;
+
+		snprintf(output_path, sizeof(output_path), "%s/%s",
+				 log_opts.basedir,
+				 state->check->report_filename);
+
+		/*
+		 * Make sure we have a buffer to save reports to now that we found a
+		 * first failing check.
+		 */
+		if (*state->report == NULL)
+			*state->report = createPQExpBuffer();
+
+		/*
+		 * If this is the first time we see an error for the check in question
+		 * then print a status message of the failure.
+		 */
+		if (!state->result)
+		{
+			pg_log(PG_REPORT, "    failed check: %s", _(state->check->status));
+			appendPQExpBuffer(*state->report, "\n%s\n%s    %s\n",
+							  _(state->check->report_text),
+							  _("A list of the problem columns is in the file:"),
+							  output_path);
+		}
+		state->result = true;
+
+		i_nspname = PQfnumber(res, "nspname");
+		i_relname = PQfnumber(res, "relname");
+		i_attname = PQfnumber(res, "attname");
+
+		for (int rowno = 0; rowno < ntups; rowno++)
+		{
+			if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
+				pg_fatal("could not open file \"%s\": %m", output_path);
+
+			if (!db_used)
+			{
+				fprintf(script, "In database: %s\n", dbinfo->db_name);
+				db_used = true;
+			}
+			fprintf(script, "  %s.%s.%s\n",
+					PQgetvalue(res, rowno, i_nspname),
+					PQgetvalue(res, rowno, i_relname),
+					PQgetvalue(res, rowno, i_attname));
+		}
+
+		if (script)
+		{
+			fclose(script);
+			script = NULL;
+		}
+	}
+}
+
 /*
  * check_for_data_types_usage()
  *	Detect whether there are any stored columns depending on given type(s)
@@ -336,11 +463,12 @@ static DataTypesUsageChecks data_types_usage_checks[] =
 static void
 check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
 {
-	bool		found = false;
-	bool	   *results;
-	PQExpBufferData report;
 	DataTypesUsageChecks *tmp = checks;
 	int			n_data_types_usage_checks = 0;
+	AsyncTask  *task = async_task_create();
+	char	  **queries = NULL;
+	struct data_type_check_state *states;
+	PQExpBuffer report = NULL;
 
 	prep_status("Checking for data type usage");
 
@@ -352,176 +480,61 @@ check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
 	}
 
 	/* Prepare an array to store the results of checks in */
-	results = pg_malloc0(sizeof(bool) * n_data_types_usage_checks);
+	queries = pg_malloc0(sizeof(char *) * n_data_types_usage_checks);
+	states = pg_malloc0(sizeof(struct data_type_check_state) * n_data_types_usage_checks);
 
-	/*
-	 * Connect to each database in the cluster and run all defined checks
-	 * against that database before trying the next one.
-	 */
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int i = 0; i < n_data_types_usage_checks; i++)
 	{
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
+		DataTypesUsageChecks *check = &checks[i];
 
-		for (int checknum = 0; checknum < n_data_types_usage_checks; checknum++)
+		if (check->threshold_version == MANUAL_CHECK)
 		{
-			PGresult   *res;
-			int			ntups;
-			int			i_nspname;
-			int			i_relname;
-			int			i_attname;
-			FILE	   *script = NULL;
-			bool		db_used = false;
-			char		output_path[MAXPGPATH];
-			DataTypesUsageChecks *cur_check = &checks[checknum];
-
-			if (cur_check->threshold_version == MANUAL_CHECK)
-			{
-				Assert(cur_check->version_hook);
-
-				/*
-				 * Make sure that the check applies to the current cluster
-				 * version and skip if not. If no check hook has been defined
-				 * we run the check for all versions.
-				 */
-				if (!cur_check->version_hook(cluster))
-					continue;
-			}
-			else if (cur_check->threshold_version != ALL_VERSIONS)
-			{
-				if (GET_MAJOR_VERSION(cluster->major_version) > cur_check->threshold_version)
-					continue;
-			}
-			else
-				Assert(cur_check->threshold_version == ALL_VERSIONS);
-
-			snprintf(output_path, sizeof(output_path), "%s/%s",
-					 log_opts.basedir,
-					 cur_check->report_filename);
+			Assert(check->version_hook);
 
 			/*
-			 * The type(s) of interest might be wrapped in a domain, array,
-			 * composite, or range, and these container types can be nested
-			 * (to varying extents depending on server version, but that's not
-			 * of concern here).  To handle all these cases we need a
-			 * recursive CTE.
+			 * Make sure that the check applies to the current cluster version
+			 * and skip it if not.
 			 */
-			res = executeQueryOrDie(conn,
-									"WITH RECURSIVE oids AS ( "
-			/* start with the type(s) returned by base_query */
-									"	%s "
-									"	UNION ALL "
-									"	SELECT * FROM ( "
-			/* inner WITH because we can only reference the CTE once */
-									"		WITH x AS (SELECT oid FROM oids) "
-			/* domains on any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
-									"			UNION ALL "
-			/* arrays over any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
-									"			UNION ALL "
-			/* composite types containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
-									"			WHERE t.typtype = 'c' AND "
-									"				  t.oid = c.reltype AND "
-									"				  c.oid = a.attrelid AND "
-									"				  NOT a.attisdropped AND "
-									"				  a.atttypid = x.oid "
-									"			UNION ALL "
-			/* ranges containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
-									"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
-									"	) foo "
-									") "
-			/* now look for stored columns of any such type */
-									"SELECT n.nspname, c.relname, a.attname "
-									"FROM	pg_catalog.pg_class c, "
-									"		pg_catalog.pg_namespace n, "
-									"		pg_catalog.pg_attribute a "
-									"WHERE	c.oid = a.attrelid AND "
-									"		NOT a.attisdropped AND "
-									"		a.atttypid IN (SELECT oid FROM oids) AND "
-									"		c.relkind IN ("
-									CppAsString2(RELKIND_RELATION) ", "
-									CppAsString2(RELKIND_MATVIEW) ", "
-									CppAsString2(RELKIND_INDEX) ") AND "
-									"		c.relnamespace = n.oid AND "
-			/* exclude possible orphaned temp tables */
-									"		n.nspname !~ '^pg_temp_' AND "
-									"		n.nspname !~ '^pg_toast_temp_' AND "
-			/* exclude system catalogs, too */
-									"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
-									cur_check->base_query);
-
-			ntups = PQntuples(res);
+			if (!check->version_hook(cluster))
+				continue;
+		}
+		else if (check->threshold_version != ALL_VERSIONS)
+		{
+			if (GET_MAJOR_VERSION(cluster->major_version) > check->threshold_version)
+				continue;
+		}
+		else
+			Assert(check->threshold_version == ALL_VERSIONS);
 
-			/*
-			 * The datatype was found, so extract the data and log to the
-			 * requested filename. We need to open the file for appending
-			 * since the check might have already found the type in another
-			 * database earlier in the loop.
-			 */
-			if (ntups)
-			{
-				/*
-				 * Make sure we have a buffer to save reports to now that we
-				 * found a first failing check.
-				 */
-				if (!found)
-					initPQExpBuffer(&report);
-				found = true;
-
-				/*
-				 * If this is the first time we see an error for the check in
-				 * question then print a status message of the failure.
-				 */
-				if (!results[checknum])
-				{
-					pg_log(PG_REPORT, "    failed check: %s", _(cur_check->status));
-					appendPQExpBuffer(&report, "\n%s\n%s    %s\n",
-									  _(cur_check->report_text),
-									  _("A list of the problem columns is in the file:"),
-									  output_path);
-				}
-				results[checknum] = true;
-
-				i_nspname = PQfnumber(res, "nspname");
-				i_relname = PQfnumber(res, "relname");
-				i_attname = PQfnumber(res, "attname");
-
-				for (int rowno = 0; rowno < ntups; rowno++)
-				{
-					if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
-						pg_fatal("could not open file \"%s\": %m", output_path);
-
-					if (!db_used)
-					{
-						fprintf(script, "In database: %s\n", active_db->db_name);
-						db_used = true;
-					}
-					fprintf(script, "  %s.%s.%s\n",
-							PQgetvalue(res, rowno, i_nspname),
-							PQgetvalue(res, rowno, i_relname),
-							PQgetvalue(res, rowno, i_attname));
-				}
-
-				if (script)
-				{
-					fclose(script);
-					script = NULL;
-				}
-			}
+		queries[i] = data_type_check_query(i);
 
-			PQclear(res);
-		}
+		states[i].check = &data_types_usage_checks[i];
+		states[i].report = &report;
 
-		PQfinish(conn);
+		async_task_add_step(task, queries[i],
+							data_type_check_process, true, &states[i]);
 	}
 
-	if (found)
-		pg_fatal("Data type checks failed: %s", report.data);
+	/*
+	 * Connect to each database in the cluster and run all defined checks
+	 * against that database before trying the next one.
+	 */
+	async_task_run(task, cluster);
+	async_task_free(task);
 
-	pg_free(results);
+	if (report)
+	{
+		pg_fatal("Data type checks failed: %s", report->data);
+		destroyPQExpBuffer(report);
+	}
+
+	for (int i = 0; i < n_data_types_usage_checks; i++)
+	{
+		if (queries[i])
+			pg_free(queries[i]);
+	}
+	pg_free(queries);
+	pg_free(states);
 
 	check_ok();
 }
-- 
2.39.3 (Apple Git-146)

v8-0007-parallelize-isn-and-int8-passing-mismatch-check-i.patchtext/plain; charset=us-asciiDownload
From 55b17aa05ade88e31955a19fd8b21c82815ed13f Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:00:20 -0500
Subject: [PATCH v8 07/11] parallelize isn and int8 passing mismatch check in
 pg_upgrade

---
 src/bin/pg_upgrade/check.c | 80 +++++++++++++++++++-------------------
 1 file changed, 39 insertions(+), 41 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 150c101d1c..5642404a2f 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1207,6 +1207,34 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 	check_ok();
 }
 
+static void
+isn_and_int8_passing_mismatch_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_proname = PQfnumber(res, "proname");
+	FILE	  **script = (FILE **) arg;
+	char		output_path[MAXPGPATH];
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "contrib_isn_and_int8_pass_by_value.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL && (*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_proname));
+	}
+}
 
 /*
  *	check_for_isn_and_int8_passing_mismatch()
@@ -1218,9 +1246,14 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 static void
 check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
+	AsyncTask  *task;
 	char		output_path[MAXPGPATH];
+	const char *query = "SELECT n.nspname, p.proname "
+		"FROM   pg_catalog.pg_proc p, "
+		"       pg_catalog.pg_namespace n "
+		"WHERE  p.pronamespace = n.oid AND "
+		"       p.probin = '$libdir/isn'";
 
 	prep_status("Checking for contrib/isn with bigint-passing mismatch");
 
@@ -1236,46 +1269,11 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "contrib_isn_and_int8_pass_by_value.txt");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_proname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* Find any functions coming from contrib/isn */
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, p.proname "
-								"FROM	pg_catalog.pg_proc p, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	p.pronamespace = n.oid AND "
-								"		p.probin = '$libdir/isn'");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_proname = PQfnumber(res, "proname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_proname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	task = async_task_create();
+	async_task_add_step(task, query,
+						isn_and_int8_passing_mismatch_process, true, &script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v8-0008-parallelize-user-defined-postfix-ops-check-in-pg_.patchtext/plain; charset=us-asciiDownload
From 851c31787de161b427de661881e8cce5d7a9f687 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:12:49 -0500
Subject: [PATCH v8 08/11] parallelize user defined postfix ops check in
 pg_upgrade

---
 src/bin/pg_upgrade/check.c | 131 +++++++++++++++++++------------------
 1 file changed, 66 insertions(+), 65 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 5642404a2f..25cd10c000 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1291,15 +1291,76 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 		check_ok();
 }
 
+static void
+user_defined_postfix_ops_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	FILE	  **script = (FILE **) arg;
+	char		output_path[MAXPGPATH];
+	int			ntups = PQntuples(res);
+	bool		db_used = false;
+	int			i_oproid = PQfnumber(res, "oproid");
+	int			i_oprnsp = PQfnumber(res, "oprnsp");
+	int			i_oprname = PQfnumber(res, "oprname");
+	int			i_typnsp = PQfnumber(res, "typnsp");
+	int			i_typname = PQfnumber(res, "typname");
+
+	if (!ntups)
+		return;
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "postfix_ops.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL &&
+			(*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
+				PQgetvalue(res, rowno, i_oproid),
+				PQgetvalue(res, rowno, i_oprnsp),
+				PQgetvalue(res, rowno, i_oprname),
+				PQgetvalue(res, rowno, i_typnsp),
+				PQgetvalue(res, rowno, i_typname));
+	}
+}
+
 /*
  * Verify that no user defined postfix operators exist.
  */
 static void
 check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
+	const char *query;
+
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	query = "SELECT o.oid AS oproid, "
+		"       n.nspname AS oprnsp, "
+		"       o.oprname, "
+		"       tn.nspname AS typnsp, "
+		"       t.typname "
+		"FROM pg_catalog.pg_operator o, "
+		"     pg_catalog.pg_namespace n, "
+		"     pg_catalog.pg_type t, "
+		"     pg_catalog.pg_namespace tn "
+		"WHERE o.oprnamespace = n.oid AND "
+		"      o.oprleft = t.oid AND "
+		"      t.typnamespace = tn.oid AND "
+		"      o.oprright = 0 AND "
+		"      o.oid >= 16384";
 
 	prep_status("Checking for user-defined postfix operators");
 
@@ -1307,70 +1368,10 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "postfix_ops.txt");
 
-	/* Find any user defined postfix operators */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_oproid,
-					i_oprnsp,
-					i_oprname,
-					i_typnsp,
-					i_typname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT o.oid AS oproid, "
-								"       n.nspname AS oprnsp, "
-								"       o.oprname, "
-								"       tn.nspname AS typnsp, "
-								"       t.typname "
-								"FROM pg_catalog.pg_operator o, "
-								"     pg_catalog.pg_namespace n, "
-								"     pg_catalog.pg_type t, "
-								"     pg_catalog.pg_namespace tn "
-								"WHERE o.oprnamespace = n.oid AND "
-								"      o.oprleft = t.oid AND "
-								"      t.typnamespace = tn.oid AND "
-								"      o.oprright = 0 AND "
-								"      o.oid >= 16384");
-		ntups = PQntuples(res);
-		i_oproid = PQfnumber(res, "oproid");
-		i_oprnsp = PQfnumber(res, "oprnsp");
-		i_oprname = PQfnumber(res, "oprname");
-		i_typnsp = PQfnumber(res, "typnsp");
-		i_typname = PQfnumber(res, "typname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
-					PQgetvalue(res, rowno, i_oproid),
-					PQgetvalue(res, rowno, i_oprnsp),
-					PQgetvalue(res, rowno, i_oprname),
-					PQgetvalue(res, rowno, i_typnsp),
-					PQgetvalue(res, rowno, i_typname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_add_step(task, query,
+						user_defined_postfix_ops_process, true, &script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v8-0009-parallelize-incompatible-polymorphics-check-in-pg.patchtext/plain; charset=us-asciiDownload
From 5792475d64639dfbf171a69c6e4f16f50a8d97c6 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:30:19 -0500
Subject: [PATCH v8 09/11] parallelize incompatible polymorphics check in
 pg_upgrade

---
 src/bin/pg_upgrade/check.c       | 155 ++++++++++++++++---------------
 src/tools/pgindent/typedefs.list |   1 +
 2 files changed, 81 insertions(+), 75 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 25cd10c000..af7d093581 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1387,6 +1387,38 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 		check_ok();
 }
 
+typedef struct incompat_polymorphics_state
+{
+	FILE	   *script;
+	char		output_path[MAXPGPATH];
+} incompat_polymorphics_state;
+
+static void
+incompat_polymorphics_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	incompat_polymorphics_state *state = (incompat_polymorphics_state *) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_objkind = PQfnumber(res, "objkind");
+	int			i_objname = PQfnumber(res, "objname");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (state->script == NULL &&
+			(state->script = fopen_priv(state->output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state->output_path);
+		if (!db_used)
+		{
+			fprintf(state->script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+
+		fprintf(state->script, "  %s: %s\n",
+				PQgetvalue(res, rowno, i_objkind),
+				PQgetvalue(res, rowno, i_objname));
+	}
+}
+
 /*
  *	check_for_incompatible_polymorphics()
  *
@@ -1396,14 +1428,15 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 static void
 check_for_incompatible_polymorphics(ClusterInfo *cluster)
 {
-	PGresult   *res;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
 	PQExpBufferData old_polymorphics;
+	AsyncTask  *task = async_task_create();
+	incompat_polymorphics_state state;
+	char	   *query;
 
 	prep_status("Checking for incompatible polymorphic functions");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	state.script = NULL;
+	snprintf(state.output_path, sizeof(state.output_path), "%s/%s",
 			 log_opts.basedir,
 			 "incompatible_polymorphics.txt");
 
@@ -1427,80 +1460,51 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 							 ", 'array_positions(anyarray,anyelement)'"
 							 ", 'width_bucket(anyelement,anyarray)'");
 
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		bool		db_used = false;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-		int			ntups;
-		int			i_objkind,
-					i_objname;
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-		/* Aggregate transition functions */
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Aggregate final functions */
-								"UNION ALL "
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Operators */
-								"UNION ALL "
-								"SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
-								"FROM pg_operator AS op "
-								"WHERE op.oid >= 16384 "
-								"AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
-								old_polymorphics.data,
-								old_polymorphics.data,
-								old_polymorphics.data);
-
-		ntups = PQntuples(res);
-
-		i_objkind = PQfnumber(res, "objkind");
-		i_objname = PQfnumber(res, "objname");
-
-		for (int rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
 
-			fprintf(script, "  %s: %s\n",
-					PQgetvalue(res, rowno, i_objkind),
-					PQgetvalue(res, rowno, i_objname));
-		}
+	/* Aggregate transition functions */
+	query = psprintf("SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					 "FROM pg_proc AS p "
+					 "JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					 "JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
+					 "WHERE p.oid >= 16384 "
+					 "AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Aggregate final functions */
+					 "UNION ALL "
+					 "SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					 "FROM pg_proc AS p "
+					 "JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					 "JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
+					 "WHERE p.oid >= 16384 "
+					 "AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Operators */
+					 "UNION ALL "
+					 "SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
+					 "FROM pg_operator AS op "
+					 "WHERE op.oid >= 16384 "
+					 "AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
+					 old_polymorphics.data,
+					 old_polymorphics.data,
+					 old_polymorphics.data);
 
-		PQclear(res);
-		PQfinish(conn);
-	}
+	async_task_add_step(task, query,
+						incompat_polymorphics_process, true, &state);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
-	if (script)
+	if (state.script)
 	{
-		fclose(script);
+		fclose(state.script);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains user-defined objects that refer to internal\n"
 				 "polymorphic functions with arguments of type \"anyarray\" or \"anyelement\".\n"
@@ -1508,12 +1512,13 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 				 "afterwards, changing them to refer to the new corresponding functions with\n"
 				 "arguments of type \"anycompatiblearray\" and \"anycompatible\".\n"
 				 "A list of the problematic objects is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", state.output_path);
 	}
 	else
 		check_ok();
 
 	termPQExpBuffer(&old_polymorphics);
+	pg_free(query);
 }
 
 /*
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ec8106329d..03be80931e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3545,6 +3545,7 @@ hstoreUpgrade_t
 hyperLogLogState
 ifState
 import_error_callback_arg
+incompat_polymorphics_state
 indexed_tlist
 inet
 inetKEY
-- 
2.39.3 (Apple Git-146)

v8-0010-parallelize-tables-with-oids-check-in-pg_upgrade.patchtext/plain; charset=us-asciiDownload
From be4bcc9e9b41c0f0aff9eecb6dfff82f9765b9b8 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:42:22 -0500
Subject: [PATCH v8 10/11] parallelize tables with oids check in pg_upgrade

---
 src/bin/pg_upgrade/check.c | 85 +++++++++++++++++++-------------------
 1 file changed, 43 insertions(+), 42 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index af7d093581..4156257843 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1521,15 +1521,53 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 	pg_free(query);
 }
 
+static void
+with_oids_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	char		output_path[MAXPGPATH];
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+	FILE	  **script = (FILE **) arg;
+
+	if (!ntups)
+		return;
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "tables_with_oids.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL && (*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_relname));
+	}
+}
+
 /*
  * Verify that no tables are declared WITH OIDS.
  */
 static void
 check_for_tables_with_oids(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
+	const char *query = "SELECT n.nspname, c.relname "
+		"FROM   pg_catalog.pg_class c, "
+		"       pg_catalog.pg_namespace n "
+		"WHERE  c.relnamespace = n.oid AND "
+		"       c.relhasoids AND"
+		"       n.nspname NOT IN ('pg_catalog')";
 
 	prep_status("Checking for tables WITH OIDS");
 
@@ -1537,47 +1575,10 @@ check_for_tables_with_oids(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "tables_with_oids.txt");
 
-	/* Find any tables declared WITH OIDS */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_relname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, c.relname "
-								"FROM	pg_catalog.pg_class c, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	c.relnamespace = n.oid AND "
-								"		c.relhasoids AND"
-								"       n.nspname NOT IN ('pg_catalog')");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_relname = PQfnumber(res, "relname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_relname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_add_step(task, query,
+						with_oids_process, true, &script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v8-0011-parallelize-user-defined-encoding-conversions-che.patchtext/plain; charset=us-asciiDownload
From b3071c2c4bf29e09b996ecdf581056fd665b3f17 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:52:13 -0500
Subject: [PATCH v8 11/11] parallelize user defined encoding conversions check
 in pg_upgrade

---
 src/bin/pg_upgrade/check.c | 102 +++++++++++++++++++------------------
 1 file changed, 53 insertions(+), 49 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 4156257843..6910d18713 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1652,15 +1652,51 @@ check_for_pg_role_prefix(ClusterInfo *cluster)
 		check_ok();
 }
 
+static void
+user_defined_encoding_conversions_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	FILE	  **script = (FILE **) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	char		output_path[MAXPGPATH];
+	int			i_conoid = PQfnumber(res, "conoid");
+	int			i_conname = PQfnumber(res, "conname");
+	int			i_nspname = PQfnumber(res, "nspname");
+
+	if (!ntups)
+		return;
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "encoding_conversions.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL &&
+			(*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  (oid=%s) %s.%s\n",
+				PQgetvalue(res, rowno, i_conoid),
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_conname));
+	}
+}
+
 /*
  * Verify that no user-defined encoding conversions exist.
  */
 static void
 check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
+	const char *query;
 
 	prep_status("Checking for user-defined encoding conversions");
 
@@ -1668,55 +1704,23 @@ check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "encoding_conversions.txt");
 
-	/* Find any user defined encoding conversions */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_conoid,
-					i_conname,
-					i_nspname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT c.oid as conoid, c.conname, n.nspname "
-								"FROM pg_catalog.pg_conversion c, "
-								"     pg_catalog.pg_namespace n "
-								"WHERE c.connamespace = n.oid AND "
-								"      c.oid >= 16384");
-		ntups = PQntuples(res);
-		i_conoid = PQfnumber(res, "conoid");
-		i_conname = PQfnumber(res, "conname");
-		i_nspname = PQfnumber(res, "nspname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s\n",
-					PQgetvalue(res, rowno, i_conoid),
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_conname));
-		}
-
-		PQclear(res);
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	query = "SELECT c.oid as conoid, c.conname, n.nspname "
+		"FROM pg_catalog.pg_conversion c, "
+		"     pg_catalog.pg_namespace n "
+		"WHERE c.connamespace = n.oid AND "
+		"      c.oid >= 16384";
 
-		PQfinish(conn);
-	}
+	async_task_add_step(task, query,
+						user_defined_encoding_conversions_process, true,
+						&script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

#22Corey Huinker
corey.huinker@gmail.com
In reply to: Nathan Bossart (#21)
Re: optimizing pg_upgrade's once-in-each-database steps

On Tue, Aug 6, 2024 at 3:20 PM Nathan Bossart <nathandbossart@gmail.com>
wrote:

On Sun, Aug 04, 2024 at 07:19:57PM +0100, Ilya Gladyshev wrote:

-- End of review --

Thanks for the review. I've attempted to address all your feedback in v8
of the patch set. I think the names could still use some work, but I
wanted to get the main structure in place before trying to fix them.

I think the underlying mechanism is basically solid, but I have one
question: isn't this the ideal case for using libpq pipelining? That would
allow subsequent tasks to launch while the main loop slowly gets around to
clearing off completed tasks on some other connection.

#23Nathan Bossart
nathandbossart@gmail.com
In reply to: Corey Huinker (#22)
Re: optimizing pg_upgrade's once-in-each-database steps

On Thu, Aug 08, 2024 at 06:18:38PM -0400, Corey Huinker wrote:

I think the underlying mechanism is basically solid, but I have one
question: isn't this the ideal case for using libpq pipelining? That would
allow subsequent tasks to launch while the main loop slowly gets around to
clearing off completed tasks on some other connection.

I'll admit I hadn't really considered pipelining, but I'm tempted to say
that it's probably not worth the complexity. Not only do most of the tasks
have only one step, but even tasks like the data types check are unlikely
to require more than a few queries for upgrades from supported versions.
Furthermore, most of the callbacks should do almost nothing for a given
upgrade, and since pg_upgrade runs on the server, client/server round-trip
time should be pretty low.

Perhaps pipelining would make more sense if we consolidated the tasks a bit
better, but when I last looked into that, I didn't see a ton of great
opportunities that would help anything except for upgrades from really old
versions. Even then, I'm not sure if pipelining is worth it.

--
nathan

#24Corey Huinker
corey.huinker@gmail.com
In reply to: Nathan Bossart (#23)
Re: optimizing pg_upgrade's once-in-each-database steps

I'll admit I hadn't really considered pipelining, but I'm tempted to say
that it's probably not worth the complexity. Not only do most of the tasks
have only one step, but even tasks like the data types check are unlikely
to require more than a few queries for upgrades from supported versions.

Can you point me to a complex multi-step task that you think wouldn't work
for pipelining? My skimming of the other patches all seemed to be one query
with one result set to be processed by one callback.

Furthermore, most of the callbacks should do almost nothing for a given

upgrade, and since pg_upgrade runs on the server, client/server round-trip
time should be pretty low.

To my mind, that makes pipelining make more sense, you throw out N queries,
most of which are trivial, and by the time you cycle back around and start
digesting result sets via callbacks, more of the queries have finished
because they were waiting on the query ahead of them in the pipeline, not
waiting on a callback to finish consuming its assigned result set and then
launching the next task query.

Perhaps pipelining would make more sense if we consolidated the tasks a bit
better, but when I last looked into that, I didn't see a ton of great
opportunities that would help anything except for upgrades from really old
versions. Even then, I'm not sure if pipelining is worth it.

I think you'd want to do the opposite of consolidating the tasks. If
anything, you'd want to break them down in known single-query operations,
and if the callback function for one of them happens to queue up a
subsequent query (with subsequent callback) then so be it.

#25Nathan Bossart
nathandbossart@gmail.com
In reply to: Corey Huinker (#24)
Re: optimizing pg_upgrade's once-in-each-database steps

On Fri, Aug 09, 2024 at 04:06:16PM -0400, Corey Huinker wrote:

I'll admit I hadn't really considered pipelining, but I'm tempted to say
that it's probably not worth the complexity. Not only do most of the tasks
have only one step, but even tasks like the data types check are unlikely
to require more than a few queries for upgrades from supported versions.

Can you point me to a complex multi-step task that you think wouldn't work
for pipelining? My skimming of the other patches all seemed to be one query
with one result set to be processed by one callback.

I think it would work fine. I'm just not sure it's worth it, especially
for tasks that run one exactly one query in each connection.

Furthermore, most of the callbacks should do almost nothing for a given
upgrade, and since pg_upgrade runs on the server, client/server round-trip
time should be pretty low.

To my mind, that makes pipelining make more sense, you throw out N queries,
most of which are trivial, and by the time you cycle back around and start
digesting result sets via callbacks, more of the queries have finished
because they were waiting on the query ahead of them in the pipeline, not
waiting on a callback to finish consuming its assigned result set and then
launching the next task query.

My assumption is that the "waiting for a callback before launching the next
query" time will typically be pretty short in practice. I could try
measuring it...

Perhaps pipelining would make more sense if we consolidated the tasks a bit
better, but when I last looked into that, I didn't see a ton of great
opportunities that would help anything except for upgrades from really old
versions. Even then, I'm not sure if pipelining is worth it.

I think you'd want to do the opposite of consolidating the tasks. If
anything, you'd want to break them down in known single-query operations,
and if the callback function for one of them happens to queue up a
subsequent query (with subsequent callback) then so be it.

By "consolidating," I mean combining tasks into fewer tasks with additional
steps. This would allow us to reuse connections instead of creating N
connections for every single query. If we used a task per query, I'd
expect pipelining to provide zero benefit.

--
nathan

#26Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#25)
Re: optimizing pg_upgrade's once-in-each-database steps

On Sat, Aug 10, 2024 at 10:17:27AM -0500, Nathan Bossart wrote:

On Fri, Aug 09, 2024 at 04:06:16PM -0400, Corey Huinker wrote:

Furthermore, most of the callbacks should do almost nothing for a given
upgrade, and since pg_upgrade runs on the server, client/server round-trip
time should be pretty low.

To my mind, that makes pipelining make more sense, you throw out N queries,
most of which are trivial, and by the time you cycle back around and start
digesting result sets via callbacks, more of the queries have finished
because they were waiting on the query ahead of them in the pipeline, not
waiting on a callback to finish consuming its assigned result set and then
launching the next task query.

My assumption is that the "waiting for a callback before launching the next
query" time will typically be pretty short in practice. I could try
measuring it...

Another option might be to combine all the queries for a task into a single
string and then send that in one PQsendQuery() call. That may be a simpler
way to eliminate the time between queries.

--
nathan

#27Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#26)
11 attachment(s)
Re: optimizing pg_upgrade's once-in-each-database steps

On Sat, Aug 10, 2024 at 10:35:46AM -0500, Nathan Bossart wrote:

Another option might be to combine all the queries for a task into a single
string and then send that in one PQsendQuery() call. That may be a simpler
way to eliminate the time between queries.

I tried this out and didn't see any difference in my tests. However, the
idea seems sound, and I could remove ~40 lines of code by doing this and by
making the search_path query an implicit first step (instead of its own
state). So, here's a v9 of the patch set with those changes.

--
nathan

Attachments:

v9-0001-introduce-framework-for-parallelizing-pg_upgrade-.patchtext/plain; charset=us-asciiDownload
From 8d8577b3dd1bbdbe584816dfb9045a5988862412 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 11:02:44 -0500
Subject: [PATCH v9 01/11] introduce framework for parallelizing pg_upgrade
 tasks

---
 src/bin/pg_upgrade/Makefile      |   1 +
 src/bin/pg_upgrade/async.c       | 425 +++++++++++++++++++++++++++++++
 src/bin/pg_upgrade/meson.build   |   1 +
 src/bin/pg_upgrade/pg_upgrade.h  |  14 +
 src/tools/pgindent/typedefs.list |   4 +
 5 files changed, 445 insertions(+)
 create mode 100644 src/bin/pg_upgrade/async.c

diff --git a/src/bin/pg_upgrade/Makefile b/src/bin/pg_upgrade/Makefile
index bde91e2beb..3bc4f5d740 100644
--- a/src/bin/pg_upgrade/Makefile
+++ b/src/bin/pg_upgrade/Makefile
@@ -12,6 +12,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	$(WIN32RES) \
+	async.o \
 	check.o \
 	controldata.o \
 	dump.o \
diff --git a/src/bin/pg_upgrade/async.c b/src/bin/pg_upgrade/async.c
new file mode 100644
index 0000000000..18a0b01f79
--- /dev/null
+++ b/src/bin/pg_upgrade/async.c
@@ -0,0 +1,425 @@
+/*
+ * async.c
+ *		parallelization via libpq's async APIs
+ *
+ * This framework provides an efficient way of running the various
+ * once-in-each-database tasks required by pg_upgrade.  Specifically, it
+ * parallelizes these tasks by managing a simple state machine of
+ * user_opts.jobs slots and using libpq's asynchronous APIs to establish the
+ * connections and run the queries.  Callers simply need to create a callback
+ * function and build/execute an AsyncTask.  A simple example follows:
+ *
+ *		static void
+ *		my_process_cb(DbInfo *dbinfo, PGresult *res, void *arg)
+ *		{
+ *			for (int i = 0; i < PQntuples(res); i++)
+ *			{
+ *				... process results ...
+ *			}
+ *		}
+ *
+ *		void
+ *		my_task(ClusterInfo *cluster)
+ *		{
+ *			AsyncTask  *task = async_task_create();
+ *
+ *			async_task_add_step(task,
+ *								"... query text ...",
+ *								my_process_cb,
+ *								true,	// let the task free the PGresult
+ *								NULL);	// "arg" pointer for the callbacks
+ *			async_task_run(task, cluster);
+ *			async_task_free(task);
+ *		}
+ *
+ * Note that multiple steps can be added to a given task.  When there are
+ * multiple steps, the task will run all of the steps consecutively in the same
+ * database connection before freeing the connection and moving on.  In other
+ * words, it only ever initiates one connection to each database in the
+ * cluster for a given run.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ * src/bin/pg_upgrade/async.c
+ */
+
+#include "postgres_fe.h"
+
+#include "common/connect.h"
+#include "fe_utils/string_utils.h"
+#include "pg_upgrade.h"
+
+/*
+ * dbs_complete stores the number of databases that we have completed
+ * processing.  When this value equals the number of databases in the cluster,
+ * the task is finished.
+ */
+static int	dbs_complete;
+
+/*
+ * dbs_processing stores the index of the next database in the cluster's array
+ * of databases that will be picked up for processing.  It will always be
+ * greater than or equal to dbs_complete.
+ */
+static int	dbs_processing;
+
+/*
+ * This struct stores all the information for a single step of a task.  All
+ * steps in a task are run in a single connection before moving on to the next
+ * database (which requires a new connection).
+ */
+typedef struct AsyncTaskCallbacks
+{
+	AsyncTaskProcessCB process_cb;	/* processes the results of the query */
+	const char *query;			/* query text */
+	bool		free_result;	/* should we free the result? */
+	void	   *arg;			/* pointer passed to each callback */
+} AsyncTaskCallbacks;
+
+/*
+ * This struct is a thin wrapper around an array of steps, i.e.,
+ * AsyncTaskCallbacks.
+ */
+typedef struct AsyncTask
+{
+	AsyncTaskCallbacks *cbs;
+	int			num_cb_sets;
+} AsyncTask;
+
+/*
+ * The different states for a parallel slot.
+ */
+typedef enum
+{
+	FREE,						/* slot available for use in a new database */
+	CONNECTING,					/* waiting for connection to be established */
+	RUNNING_QUERIES,			/* running/processing queries in the task */
+} AsyncSlotState;
+
+/*
+ * We maintain an array of user_opts.jobs slots to execute the task.
+ */
+typedef struct
+{
+	AsyncSlotState state;		/* state of the slot */
+	int			db;				/* index of the database assigned to slot */
+	int			query;			/* index of the current query to process */
+	PGconn	   *conn;			/* current connection managed by slot */
+} AsyncSlot;
+
+/*
+ * Initializes an AsyncTask.
+ */
+AsyncTask *
+async_task_create(void)
+{
+	AsyncTask  *task = pg_malloc0(sizeof(AsyncTask));
+
+	/* All tasks must first set a secure search_path. */
+	async_task_add_step(task, ALWAYS_SECURE_SEARCH_PATH_SQL, NULL, true, NULL);
+	return task;
+}
+
+/*
+ * Frees all storage associated with an AsyncTask.
+ */
+void
+async_task_free(AsyncTask *task)
+{
+	if (task->cbs)
+		pg_free(task->cbs);
+
+	pg_free(task);
+}
+
+/*
+ * Adds a step to an AsyncTask.  The steps will be executed in each database in
+ * the order in which they are added.
+ *
+ *	task: task object that must have been initialized via async_task_create()
+ *	query: the query text
+ *	process_cb: function that processes the results of the query
+ *	free_result: should we free the PGresult, or leave it to the caller?
+ *	arg: pointer to task-specific data that is passed to each callback
+ */
+void
+async_task_add_step(AsyncTask *task, const char *query,
+					AsyncTaskProcessCB process_cb, bool free_result,
+					void *arg)
+{
+	AsyncTaskCallbacks *new_cbs;
+
+	task->cbs = pg_realloc(task->cbs,
+						   ++task->num_cb_sets * sizeof(AsyncTaskCallbacks));
+
+	new_cbs = &task->cbs[task->num_cb_sets - 1];
+	new_cbs->process_cb = process_cb;
+	new_cbs->query = query;
+	new_cbs->free_result = free_result;
+	new_cbs->arg = arg;
+}
+
+/*
+ * A simple wrapper around a pg_fatal() that includes the error message for the
+ * connection.
+ */
+static void
+conn_failure(PGconn *conn)
+{
+	pg_fatal("connection failure: %s", PQerrorMessage(conn));
+}
+
+/*
+ * Build a connection string for the slot's current database and asynchronously
+ * start a new connection, but do not wait for the connection to be
+ * established.
+ */
+static void
+start_conn(const ClusterInfo *cluster, AsyncSlot *slot)
+{
+	PQExpBufferData conn_opts;
+
+	/* Build connection string with proper quoting */
+	initPQExpBuffer(&conn_opts);
+	appendPQExpBufferStr(&conn_opts, "dbname=");
+	appendConnStrVal(&conn_opts, cluster->dbarr.dbs[slot->db].db_name);
+	appendPQExpBufferStr(&conn_opts, " user=");
+	appendConnStrVal(&conn_opts, os_info.user);
+	appendPQExpBuffer(&conn_opts, " port=%d", cluster->port);
+	if (cluster->sockdir)
+	{
+		appendPQExpBufferStr(&conn_opts, " host=");
+		appendConnStrVal(&conn_opts, cluster->sockdir);
+	}
+
+	slot->conn = PQconnectStart(conn_opts.data);
+
+	if (!slot->conn)
+		pg_fatal("failed to create connection with connection string: \"%s\"",
+				 conn_opts.data);
+
+	termPQExpBuffer(&conn_opts);
+}
+
+/*
+ * Run the process_cb callback function to process the result of a query, and
+ * free the result if the caller indicated we should do so.
+ */
+static void
+process_query_result(const ClusterInfo *cluster, AsyncSlot *slot,
+					 const AsyncTask *task)
+{
+	AsyncTaskCallbacks *cbs = &task->cbs[slot->query];
+	AsyncTaskProcessCB process_cb = cbs->process_cb;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db];
+	PGresult   *res = PQgetResult(slot->conn);
+
+	if (PQstatus(slot->conn) == CONNECTION_BAD ||
+		(PQresultStatus(res) != PGRES_TUPLES_OK &&
+		 PQresultStatus(res) != PGRES_COMMAND_OK))
+		conn_failure(slot->conn);
+
+	if (process_cb)
+		(*process_cb) (dbinfo, res, cbs->arg);
+
+	if (cbs->free_result)
+		PQclear(res);
+}
+
+/*
+ * Advances the state machine for a given slot as necessary.
+ */
+static void
+process_slot(const ClusterInfo *cluster, AsyncSlot *slot, const AsyncTask *task)
+{
+	PQExpBufferData queries;
+
+	switch (slot->state)
+	{
+		case FREE:
+
+			/*
+			 * If all of the databases in the cluster have been processed or
+			 * are currently being processed by other slots, we are done.
+			 */
+			if (dbs_processing >= cluster->dbarr.ndbs)
+				return;
+
+			/*
+			 * Claim the next database in the cluster's array and initiate a
+			 * new connection.
+			 */
+			slot->db = dbs_processing++;
+			slot->state = CONNECTING;
+			start_conn(cluster, slot);
+
+			return;
+
+		case CONNECTING:
+
+			/* Check for connection failure. */
+			if (PQconnectPoll(slot->conn) == PGRES_POLLING_FAILED)
+				conn_failure(slot->conn);
+
+			/* Check whether the connection is still establishing. */
+			if (PQconnectPoll(slot->conn) != PGRES_POLLING_OK)
+				return;
+
+			/*
+			 * Move on to running/processing the queries in the task.  We
+			 * combine all the queries and send them to the server together.
+			 */
+			slot->state = RUNNING_QUERIES;
+			initPQExpBuffer(&queries);
+			for (int i = 0; i < task->num_cb_sets; i++)
+				appendPQExpBuffer(&queries, "%s;", task->cbs[i].query);
+			if (!PQsendQuery(slot->conn, queries.data))
+				conn_failure(slot->conn);
+			termPQExpBuffer(&queries);
+
+			return;
+
+		case RUNNING_QUERIES:
+
+			/*
+			 * Process any results that are ready so that we can free up this
+			 * slot for another database as soon as possible.
+			 */
+			for (; slot->query < task->num_cb_sets; slot->query++)
+			{
+				/* If no more results are available yet, move on. */
+				if (!PQconsumeInput(slot->conn))
+					conn_failure(slot->conn);
+				if (PQisBusy(slot->conn))
+					return;
+
+				process_query_result(cluster, slot, task);
+			}
+
+			/*
+			 * If we just finished processing the result of the last step in
+			 * the task, free the slot.  We recursively call this function on
+			 * the newly-freed slot so that we can start initiating the next
+			 * connection immediately instead of waiting for the next loop
+			 * through the slots.
+			 */
+			dbs_complete++;
+			(void) PQgetResult(slot->conn);
+			PQfinish(slot->conn);
+			memset(slot, 0, sizeof(AsyncSlot));
+
+			process_slot(cluster, slot, task);
+
+			return;
+	}
+}
+
+/*
+ * Wait on the slots to either finish connecting or to receive query results if
+ * possible.  This avoids a tight loop in async_task_run().
+ */
+static void
+wait_on_slots(AsyncSlot *slots, int numslots)
+{
+	fd_set		input_mask;
+	fd_set		output_mask;
+	fd_set		except_mask;
+	int			maxFd = 0;
+
+	FD_ZERO(&input_mask);
+	FD_ZERO(&output_mask);
+	FD_ZERO(&except_mask);
+
+	for (int i = 0; i < numslots; i++)
+	{
+		int			sock;
+		bool		read = false;
+
+		switch (slots[i].state)
+		{
+			case FREE:
+
+				/*
+				 * This function should only ever see free slots as we are
+				 * finishing processing the last few databases, at which point
+				 * we don't have any databases left for them to process. We'll
+				 * never use these slots again, so we can safely ignore them.
+				 */
+				continue;
+
+			case CONNECTING:
+
+				/*
+				 * If we are waiting for the connection to establish, choose
+				 * whether to wait for reading or for writing on the socket as
+				 * appropriate.  If neither apply, just return immediately so
+				 * that we can handle the slot.
+				 */
+				{
+					PostgresPollingStatusType status;
+
+					status = PQconnectPoll(slots[i].conn);
+					if (status == PGRES_POLLING_READING)
+						read = true;
+					else if (status != PGRES_POLLING_WRITING)
+						return;
+				}
+				break;
+
+			case RUNNING_QUERIES:
+
+				/*
+				 * Once we've sent the queries, we must wait for the socket to
+				 * be read-ready.  Note that process_slot() handles calling
+				 * PQconsumeInput() as required.
+				 */
+				read = true;
+				break;
+		}
+
+		/*
+		 * If there's some problem retrieving the socket, just pretend this
+		 * slot doesn't exist.  We don't expect this to happen regularly in
+		 * practice, so it seems unlikely to cause too much harm.
+		 */
+		sock = PQsocket(slots[i].conn);
+		if (sock < 0)
+			continue;
+
+		/*
+		 * Add the socket to the set.
+		 */
+		FD_SET(sock, read ? &input_mask : &output_mask);
+		FD_SET(sock, &except_mask);
+		maxFd = Max(maxFd, sock);
+	}
+
+	/*
+	 * If we found socket(s) to wait on, wait.
+	 */
+	if (maxFd != 0)
+		(void) select(maxFd + 1, &input_mask, &output_mask, &except_mask, NULL);
+}
+
+/*
+ * Runs all the steps of the task in every database in the cluster using
+ * user_opts.jobs parallel slots.
+ */
+void
+async_task_run(const AsyncTask *task, const ClusterInfo *cluster)
+{
+	int			jobs = Max(1, user_opts.jobs);
+	AsyncSlot  *slots = pg_malloc0(sizeof(AsyncSlot) * jobs);
+
+	dbs_complete = 0;
+	dbs_processing = 0;
+
+	while (dbs_complete < cluster->dbarr.ndbs)
+	{
+		for (int i = 0; i < jobs; i++)
+			process_slot(cluster, &slots[i], task);
+
+		wait_on_slots(slots, jobs);
+	}
+
+	pg_free(slots);
+}
diff --git a/src/bin/pg_upgrade/meson.build b/src/bin/pg_upgrade/meson.build
index 9825fa3305..9eb48e176c 100644
--- a/src/bin/pg_upgrade/meson.build
+++ b/src/bin/pg_upgrade/meson.build
@@ -1,6 +1,7 @@
 # Copyright (c) 2022-2024, PostgreSQL Global Development Group
 
 pg_upgrade_sources = files(
+  'async.c',
   'check.c',
   'controldata.c',
   'dump.c',
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index cdb6e2b759..5f319af34d 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -494,3 +494,17 @@ void		parallel_transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr
 										  char *old_pgdata, char *new_pgdata,
 										  char *old_tablespace);
 bool		reap_child(bool wait_for_child);
+
+/* async.c */
+
+typedef void (*AsyncTaskProcessCB) (DbInfo *dbinfo, PGresult *res, void *arg);
+
+/* struct definition is private to async.c */
+typedef struct AsyncTask AsyncTask;
+
+AsyncTask  *async_task_create(void);
+void		async_task_add_step(AsyncTask *task, const char *query,
+								AsyncTaskProcessCB process_cb, bool free_result,
+								void *arg);
+void		async_task_run(const AsyncTask *task, const ClusterInfo *cluster);
+void		async_task_free(AsyncTask *task);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 547d14b3e7..ec8106329d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -153,6 +153,10 @@ ArrayMetaState
 ArraySubWorkspace
 ArrayToken
 ArrayType
+AsyncSlot
+AsyncSlotState
+AsyncTask
+AsyncTaskCallbacks
 AsyncQueueControl
 AsyncQueueEntry
 AsyncRequest
-- 
2.39.3 (Apple Git-146)

v9-0002-use-new-pg_upgrade-async-API-for-subscription-sta.patchtext/plain; charset=us-asciiDownload
From 2bc5d53bd2bac29dd447a286c7e6058c70bbe348 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 17:21:19 -0500
Subject: [PATCH v9 02/11] use new pg_upgrade async API for subscription state
 checks

---
 src/bin/pg_upgrade/check.c | 204 ++++++++++++++++++++-----------------
 1 file changed, 110 insertions(+), 94 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 5038231731..48f5a0c0e6 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1905,6 +1905,79 @@ check_old_cluster_for_valid_slots(void)
 	check_ok();
 }
 
+/* private state for subscription state checks */
+struct substate_info
+{
+	FILE	   *script;
+	char		output_path[MAXPGPATH];
+};
+
+/*
+ * We don't allow upgrade if there is a risk of dangling slot or origin
+ * corresponding to initial sync after upgrade.
+ *
+ * A slot/origin not created yet refers to the 'i' (initialize) state, while
+ * 'r' (ready) state refers to a slot/origin created previously but already
+ * dropped. These states are supported for pg_upgrade. The other states listed
+ * below are not supported:
+ *
+ * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state would
+ * retain a replication slot, which could not be dropped by the sync worker
+ * spawned after the upgrade because the subscription ID used for the slot name
+ * won't match anymore.
+ *
+ * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state would
+ * retain the replication origin when there is a failure in tablesync worker
+ * immediately after dropping the replication slot in the publisher.
+ *
+ * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on a
+ * relation upgraded while in this state would expect an origin ID with the OID
+ * of the subscription used before the upgrade, causing it to fail.
+ *
+ * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and SUBREL_STATE_UNKNOWN:
+ * These states are not stored in the catalog, so we need not allow these
+ * states.
+ */
+static const char *
+sub_query(void)
+{
+	return "SELECT r.srsubstate, s.subname, n.nspname, c.relname "
+		"FROM pg_catalog.pg_subscription_rel r "
+		"LEFT JOIN pg_catalog.pg_subscription s"
+		"   ON r.srsubid = s.oid "
+		"LEFT JOIN pg_catalog.pg_class c"
+		"   ON r.srrelid = c.oid "
+		"LEFT JOIN pg_catalog.pg_namespace n"
+		"   ON c.relnamespace = n.oid "
+		"WHERE r.srsubstate NOT IN ('i', 'r') "
+		"ORDER BY s.subname";
+}
+
+static void
+sub_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct substate_info *state = (struct substate_info *) arg;
+	int			ntup = PQntuples(res);
+	int			i_srsubstate = PQfnumber(res, "srsubstate");
+	int			i_subname = PQfnumber(res, "subname");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+
+	for (int i = 0; i < ntup; i++)
+	{
+		if (state->script == NULL &&
+			(state->script = fopen_priv(state->output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state->output_path);
+
+		fprintf(state->script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
+				PQgetvalue(res, i, i_srsubstate),
+				dbinfo->db_name,
+				PQgetvalue(res, i, i_subname),
+				PQgetvalue(res, i, i_nspname),
+				PQgetvalue(res, i, i_relname));
+	}
+}
+
 /*
  * check_old_cluster_subscription_state()
  *
@@ -1915,115 +1988,58 @@ check_old_cluster_for_valid_slots(void)
 static void
 check_old_cluster_subscription_state(void)
 {
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
+	struct substate_info state;
+	PGresult   *res;
+	PGconn	   *conn;
 	int			ntup;
 
 	prep_status("Checking for subscription state");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	state.script = NULL;
+	snprintf(state.output_path, sizeof(state.output_path), "%s/%s",
 			 log_opts.basedir,
 			 "subs_invalid.txt");
-	for (int dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
 
-		/* We need to check for pg_replication_origin only once. */
-		if (dbnum == 0)
-		{
-			/*
-			 * Check that all the subscriptions have their respective
-			 * replication origin.
-			 */
-			res = executeQueryOrDie(conn,
-									"SELECT d.datname, s.subname "
-									"FROM pg_catalog.pg_subscription s "
-									"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
-									"	ON o.roname = 'pg_' || s.oid "
-									"INNER JOIN pg_catalog.pg_database d "
-									"	ON d.oid = s.subdbid "
-									"WHERE o.roname IS NULL;");
-
-			ntup = PQntuples(res);
-			for (int i = 0; i < ntup; i++)
-			{
-				if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-					pg_fatal("could not open file \"%s\": %m", output_path);
-				fprintf(script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
-						PQgetvalue(res, i, 0),
-						PQgetvalue(res, i, 1));
-			}
-			PQclear(res);
-		}
-
-		/*
-		 * We don't allow upgrade if there is a risk of dangling slot or
-		 * origin corresponding to initial sync after upgrade.
-		 *
-		 * A slot/origin not created yet refers to the 'i' (initialize) state,
-		 * while 'r' (ready) state refers to a slot/origin created previously
-		 * but already dropped. These states are supported for pg_upgrade. The
-		 * other states listed below are not supported:
-		 *
-		 * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state
-		 * would retain a replication slot, which could not be dropped by the
-		 * sync worker spawned after the upgrade because the subscription ID
-		 * used for the slot name won't match anymore.
-		 *
-		 * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state
-		 * would retain the replication origin when there is a failure in
-		 * tablesync worker immediately after dropping the replication slot in
-		 * the publisher.
-		 *
-		 * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on
-		 * a relation upgraded while in this state would expect an origin ID
-		 * with the OID of the subscription used before the upgrade, causing
-		 * it to fail.
-		 *
-		 * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and
-		 * SUBREL_STATE_UNKNOWN: These states are not stored in the catalog,
-		 * so we need not allow these states.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT r.srsubstate, s.subname, n.nspname, c.relname "
-								"FROM pg_catalog.pg_subscription_rel r "
-								"LEFT JOIN pg_catalog.pg_subscription s"
-								"	ON r.srsubid = s.oid "
-								"LEFT JOIN pg_catalog.pg_class c"
-								"	ON r.srrelid = c.oid "
-								"LEFT JOIN pg_catalog.pg_namespace n"
-								"	ON c.relnamespace = n.oid "
-								"WHERE r.srsubstate NOT IN ('i', 'r') "
-								"ORDER BY s.subname");
-
-		ntup = PQntuples(res);
-		for (int i = 0; i < ntup; i++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
+	/*
+	 * Check that all the subscriptions have their respective replication
+	 * origin.  This check only needs to run once.
+	 */
+	conn = connectToServer(&old_cluster, old_cluster.dbarr.dbs[0].db_name);
+	res = executeQueryOrDie(conn,
+							"SELECT d.datname, s.subname "
+							"FROM pg_catalog.pg_subscription s "
+							"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
+							"	ON o.roname = 'pg_' || s.oid "
+							"INNER JOIN pg_catalog.pg_database d "
+							"	ON d.oid = s.subdbid "
+							"WHERE o.roname IS NULL;");
+	ntup = PQntuples(res);
+	for (int i = 0; i < ntup; i++)
+	{
+		if (state.script == NULL &&
+			(state.script = fopen_priv(state.output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state.output_path);
+		fprintf(state.script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
+				PQgetvalue(res, i, 0),
+				PQgetvalue(res, i, 1));
+	}
+	PQclear(res);
+	PQfinish(conn);
 
-			fprintf(script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
-					PQgetvalue(res, i, 0),
-					active_db->db_name,
-					PQgetvalue(res, i, 1),
-					PQgetvalue(res, i, 2),
-					PQgetvalue(res, i, 3));
-		}
+	async_task_add_step(task, sub_query(), sub_process, true, &state);
 
-		PQclear(res);
-		PQfinish(conn);
-	}
+	async_task_run(task, &old_cluster);
+	async_task_free(task);
 
-	if (script)
+	if (state.script)
 	{
-		fclose(script);
+		fclose(state.script);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains subscriptions without origin or having relations not in i (initialize) or r (ready) state.\n"
 				 "You can allow the initial sync to finish for all relations and then restart the upgrade.\n"
 				 "A list of the problematic subscriptions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", state.output_path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v9-0003-use-new-pg_upgrade-async-API-for-retrieving-relin.patchtext/plain; charset=us-asciiDownload
From 08781ccc2871d9f3105800ba5038988097b2cb0a Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:09:33 -0500
Subject: [PATCH v9 03/11] use new pg_upgrade async API for retrieving relinfos

---
 src/bin/pg_upgrade/info.c | 246 +++++++++++++++++++-------------------
 1 file changed, 120 insertions(+), 126 deletions(-)

diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 5de5e10945..5d93e9716d 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -11,6 +11,7 @@
 
 #include "access/transam.h"
 #include "catalog/pg_class_d.h"
+#include "pqexpbuffer.h"
 #include "pg_upgrade.h"
 
 static void create_rel_filename_map(const char *old_data, const char *new_data,
@@ -22,12 +23,14 @@ static void report_unmatched_relation(const RelInfo *rel, const DbInfo *db,
 static void free_db_and_rel_infos(DbInfoArr *db_arr);
 static void get_template0_info(ClusterInfo *cluster);
 static void get_db_infos(ClusterInfo *cluster);
-static void get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo);
+static char *get_rel_infos_query(void);
+static void get_rel_infos_result(DbInfo *dbinfo, PGresult *res, void *arg);
 static void free_rel_infos(RelInfoArr *rel_arr);
 static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
-static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo);
+static char *get_old_cluster_logical_slot_infos_query(void);
+static void get_old_cluster_logical_slot_infos_result(DbInfo *dbinfo, PGresult *res, void *arg);
 
 
 /*
@@ -276,7 +279,9 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
 void
 get_db_rel_and_slot_infos(ClusterInfo *cluster)
 {
-	int			dbnum;
+	AsyncTask  *task = async_task_create();
+	char	   *rel_infos_query = NULL;
+	char	   *logical_slot_infos_query = NULL;
 
 	if (cluster->dbarr.dbs != NULL)
 		free_db_and_rel_infos(&cluster->dbarr);
@@ -284,15 +289,29 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster)
 	get_template0_info(cluster);
 	get_db_infos(cluster);
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	rel_infos_query = get_rel_infos_query();
+	async_task_add_step(task,
+						rel_infos_query,
+						get_rel_infos_result,
+						true, NULL);
+
+	if (cluster == &old_cluster &&
+		GET_MAJOR_VERSION(cluster->major_version) > 1600)
 	{
-		DbInfo	   *pDbInfo = &cluster->dbarr.dbs[dbnum];
+		logical_slot_infos_query = get_old_cluster_logical_slot_infos_query();
+		async_task_add_step(task,
+							logical_slot_infos_query,
+							get_old_cluster_logical_slot_infos_result,
+							true, NULL);
+	}
 
-		get_rel_infos(cluster, pDbInfo);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
-		if (cluster == &old_cluster)
-			get_old_cluster_logical_slot_infos(pDbInfo);
-	}
+	if (rel_infos_query)
+		pg_free(rel_infos_query);
+	if (logical_slot_infos_query)
+		pg_free(logical_slot_infos_query);
 
 	if (cluster == &old_cluster)
 		pg_log(PG_VERBOSE, "\nsource databases:");
@@ -439,32 +458,12 @@ get_db_infos(ClusterInfo *cluster)
  * Note: the resulting RelInfo array is assumed to be sorted by OID.
  * This allows later processing to match up old and new databases efficiently.
  */
-static void
-get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
+static char *
+get_rel_infos_query(void)
 {
-	PGconn	   *conn = connectToServer(cluster,
-									   dbinfo->db_name);
-	PGresult   *res;
-	RelInfo    *relinfos;
-	int			ntups;
-	int			relnum;
-	int			num_rels = 0;
-	char	   *nspname = NULL;
-	char	   *relname = NULL;
-	char	   *tablespace = NULL;
-	int			i_spclocation,
-				i_nspname,
-				i_relname,
-				i_reloid,
-				i_indtable,
-				i_toastheap,
-				i_relfilenumber,
-				i_reltablespace;
-	char		query[QUERY_ALLOC];
-	char	   *last_namespace = NULL,
-			   *last_tablespace = NULL;
+	PQExpBufferData query;
 
-	query[0] = '\0';			/* initialize query string to empty */
+	initPQExpBuffer(&query);
 
 	/*
 	 * Create a CTE that collects OIDs of regular user tables and matviews,
@@ -476,34 +475,34 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * output, so we have to copy that system table.  It's easiest to do that
 	 * by treating it as a user table.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "WITH regular_heap (reloid, indtable, toastheap) AS ( "
-			 "  SELECT c.oid, 0::oid, 0::oid "
-			 "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
-			 "         ON c.relnamespace = n.oid "
-			 "  WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
-			 CppAsString2(RELKIND_MATVIEW) ") AND "
+	appendPQExpBuffer(&query,
+					  "WITH regular_heap (reloid, indtable, toastheap) AS ( "
+					  "  SELECT c.oid, 0::oid, 0::oid "
+					  "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
+					  "         ON c.relnamespace = n.oid "
+					  "  WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
+					  CppAsString2(RELKIND_MATVIEW) ") AND "
 	/* exclude possible orphaned temp tables */
-			 "    ((n.nspname !~ '^pg_temp_' AND "
-			 "      n.nspname !~ '^pg_toast_temp_' AND "
-			 "      n.nspname NOT IN ('pg_catalog', 'information_schema', "
-			 "                        'binary_upgrade', 'pg_toast') AND "
-			 "      c.oid >= %u::pg_catalog.oid) OR "
-			 "     (n.nspname = 'pg_catalog' AND "
-			 "      relname IN ('pg_largeobject') ))), ",
-			 FirstNormalObjectId);
+					  "    ((n.nspname !~ '^pg_temp_' AND "
+					  "      n.nspname !~ '^pg_toast_temp_' AND "
+					  "      n.nspname NOT IN ('pg_catalog', 'information_schema', "
+					  "                        'binary_upgrade', 'pg_toast') AND "
+					  "      c.oid >= %u::pg_catalog.oid) OR "
+					  "     (n.nspname = 'pg_catalog' AND "
+					  "      relname IN ('pg_largeobject') ))), ",
+					  FirstNormalObjectId);
 
 	/*
 	 * Add a CTE that collects OIDs of toast tables belonging to the tables
 	 * selected by the regular_heap CTE.  (We have to do this separately
 	 * because the namespace-name rules above don't work for toast tables.)
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "  toast_heap (reloid, indtable, toastheap) AS ( "
-			 "  SELECT c.reltoastrelid, 0::oid, c.oid "
-			 "  FROM regular_heap JOIN pg_catalog.pg_class c "
-			 "      ON regular_heap.reloid = c.oid "
-			 "  WHERE c.reltoastrelid != 0), ");
+	appendPQExpBufferStr(&query,
+						 "  toast_heap (reloid, indtable, toastheap) AS ( "
+						 "  SELECT c.reltoastrelid, 0::oid, c.oid "
+						 "  FROM regular_heap JOIN pg_catalog.pg_class c "
+						 "      ON regular_heap.reloid = c.oid "
+						 "  WHERE c.reltoastrelid != 0), ");
 
 	/*
 	 * Add a CTE that collects OIDs of all valid indexes on the previously
@@ -511,53 +510,61 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * Testing indisready is necessary in 9.2, and harmless in earlier/later
 	 * versions.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "  all_index (reloid, indtable, toastheap) AS ( "
-			 "  SELECT indexrelid, indrelid, 0::oid "
-			 "  FROM pg_catalog.pg_index "
-			 "  WHERE indisvalid AND indisready "
-			 "    AND indrelid IN "
-			 "        (SELECT reloid FROM regular_heap "
-			 "         UNION ALL "
-			 "         SELECT reloid FROM toast_heap)) ");
+	appendPQExpBufferStr(&query,
+						 "  all_index (reloid, indtable, toastheap) AS ( "
+						 "  SELECT indexrelid, indrelid, 0::oid "
+						 "  FROM pg_catalog.pg_index "
+						 "  WHERE indisvalid AND indisready "
+						 "    AND indrelid IN "
+						 "        (SELECT reloid FROM regular_heap "
+						 "         UNION ALL "
+						 "         SELECT reloid FROM toast_heap)) ");
 
 	/*
 	 * And now we can write the query that retrieves the data we want for each
 	 * heap and index relation.  Make sure result is sorted by OID.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "SELECT all_rels.*, n.nspname, c.relname, "
-			 "  c.relfilenode, c.reltablespace, "
-			 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
-			 "FROM (SELECT * FROM regular_heap "
-			 "      UNION ALL "
-			 "      SELECT * FROM toast_heap "
-			 "      UNION ALL "
-			 "      SELECT * FROM all_index) all_rels "
-			 "  JOIN pg_catalog.pg_class c "
-			 "      ON all_rels.reloid = c.oid "
-			 "  JOIN pg_catalog.pg_namespace n "
-			 "     ON c.relnamespace = n.oid "
-			 "  LEFT OUTER JOIN pg_catalog.pg_tablespace t "
-			 "     ON c.reltablespace = t.oid "
-			 "ORDER BY 1;");
-
-	res = executeQueryOrDie(conn, "%s", query);
-
-	ntups = PQntuples(res);
-
-	relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	appendPQExpBufferStr(&query,
+						 "SELECT all_rels.*, n.nspname, c.relname, "
+						 "  c.relfilenode, c.reltablespace, "
+						 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
+						 "FROM (SELECT * FROM regular_heap "
+						 "      UNION ALL "
+						 "      SELECT * FROM toast_heap "
+						 "      UNION ALL "
+						 "      SELECT * FROM all_index) all_rels "
+						 "  JOIN pg_catalog.pg_class c "
+						 "      ON all_rels.reloid = c.oid "
+						 "  JOIN pg_catalog.pg_namespace n "
+						 "     ON c.relnamespace = n.oid "
+						 "  LEFT OUTER JOIN pg_catalog.pg_tablespace t "
+						 "     ON c.reltablespace = t.oid "
+						 "ORDER BY 1;");
+
+	return query.data;
+}
 
-	i_reloid = PQfnumber(res, "reloid");
-	i_indtable = PQfnumber(res, "indtable");
-	i_toastheap = PQfnumber(res, "toastheap");
-	i_nspname = PQfnumber(res, "nspname");
-	i_relname = PQfnumber(res, "relname");
-	i_relfilenumber = PQfnumber(res, "relfilenode");
-	i_reltablespace = PQfnumber(res, "reltablespace");
-	i_spclocation = PQfnumber(res, "spclocation");
+static void
+get_rel_infos_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	int			ntups = PQntuples(res);
+	RelInfo    *relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	int			i_reloid = PQfnumber(res, "reloid");
+	int			i_indtable = PQfnumber(res, "indtable");
+	int			i_toastheap = PQfnumber(res, "toastheap");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+	int			i_relfilenumber = PQfnumber(res, "relfilenode");
+	int			i_reltablespace = PQfnumber(res, "reltablespace");
+	int			i_spclocation = PQfnumber(res, "spclocation");
+	int			num_rels = 0;
+	char	   *nspname = NULL;
+	char	   *relname = NULL;
+	char	   *tablespace = NULL;
+	char	   *last_namespace = NULL;
+	char	   *last_tablespace = NULL;
 
-	for (relnum = 0; relnum < ntups; relnum++)
+	for (int relnum = 0; relnum < ntups; relnum++)
 	{
 		RelInfo    *curr = &relinfos[num_rels++];
 
@@ -610,9 +617,6 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 			/* A zero reltablespace oid indicates the database tablespace. */
 			curr->tablespace = dbinfo->db_tablespace;
 	}
-	PQclear(res);
-
-	PQfinish(conn);
 
 	dbinfo->rel_arr.rels = relinfos;
 	dbinfo->rel_arr.nrels = num_rels;
@@ -634,20 +638,9 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
  * check_old_cluster_for_valid_slots() would raise a FATAL error if such slots
  * are included.
  */
-static void
-get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
+static char *
+get_old_cluster_logical_slot_infos_query(void)
 {
-	PGconn	   *conn;
-	PGresult   *res;
-	LogicalSlotInfo *slotinfos = NULL;
-	int			num_slots;
-
-	/* Logical slots can be migrated since PG17. */
-	if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1600)
-		return;
-
-	conn = connectToServer(&old_cluster, dbinfo->db_name);
-
 	/*
 	 * Fetch the logical replication slot information. The check whether the
 	 * slot is considered caught up is done by an upgrade function. This
@@ -665,18 +658,23 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
-							"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
-							"FROM pg_catalog.pg_replication_slots "
-							"WHERE slot_type = 'logical' AND "
-							"database = current_database() AND "
-							"temporary IS FALSE;",
-							user_opts.live_check ? "FALSE" :
-							"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
-							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
-							"END)");
-
-	num_slots = PQntuples(res);
+	return psprintf("SELECT slot_name, plugin, two_phase, failover, "
+					"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
+					"FROM pg_catalog.pg_replication_slots "
+					"WHERE slot_type = 'logical' AND "
+					"database = current_database() AND "
+					"temporary IS FALSE;",
+					user_opts.live_check ? "FALSE" :
+					"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
+					"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
+					"END)");
+}
+
+static void
+get_old_cluster_logical_slot_infos_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	LogicalSlotInfo *slotinfos = NULL;
+	int			num_slots = PQntuples(res);
 
 	if (num_slots)
 	{
@@ -709,14 +707,10 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 		}
 	}
 
-	PQclear(res);
-	PQfinish(conn);
-
 	dbinfo->slot_arr.slots = slotinfos;
 	dbinfo->slot_arr.nslots = num_slots;
 }
 
-
 /*
  * count_old_cluster_logical_slots()
  *
-- 
2.39.3 (Apple Git-146)

v9-0004-use-new-pg_upgrade-async-API-to-parallelize-getti.patchtext/plain; charset=us-asciiDownload
From dd92f014151f5a356e8f732eb1773d1d825baed7 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:24:35 -0500
Subject: [PATCH v9 04/11] use new pg_upgrade async API to parallelize getting
 loadable libraries

---
 src/bin/pg_upgrade/function.c | 49 +++++++++++++++++++++--------------
 1 file changed, 29 insertions(+), 20 deletions(-)

diff --git a/src/bin/pg_upgrade/function.c b/src/bin/pg_upgrade/function.c
index 7e3abed098..75e5ebb2c8 100644
--- a/src/bin/pg_upgrade/function.c
+++ b/src/bin/pg_upgrade/function.c
@@ -42,6 +42,20 @@ library_name_compare(const void *p1, const void *p2)
 					  ((const LibraryInfo *) p2)->dbnum);
 }
 
+struct loadable_libraries_state
+{
+	PGresult  **ress;
+	int			totaltups;
+};
+
+static void
+get_loadable_libraries_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct loadable_libraries_state *state = (struct loadable_libraries_state *) arg;
+
+	state->ress[dbinfo - old_cluster.dbarr.dbs] = res;
+	state->totaltups += PQntuples(res);
+}
 
 /*
  * get_loadable_libraries()
@@ -54,47 +68,41 @@ library_name_compare(const void *p1, const void *p2)
 void
 get_loadable_libraries(void)
 {
-	PGresult  **ress;
 	int			totaltups;
 	int			dbnum;
 	int			n_libinfos;
+	AsyncTask  *task = async_task_create();
+	struct loadable_libraries_state state;
+	char	   *loadable_libraries_query;
 
-	ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
-	totaltups = 0;
-
-	/* Fetch all library names, removing duplicates within each DB */
-	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
+	state.ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
+	state.totaltups = 0;
 
-		/*
-		 * Fetch all libraries containing non-built-in C functions in this DB.
-		 */
-		ress[dbnum] = executeQueryOrDie(conn,
-										"SELECT DISTINCT probin "
+	loadable_libraries_query = psprintf("SELECT DISTINCT probin "
 										"FROM pg_catalog.pg_proc "
 										"WHERE prolang = %u AND "
 										"probin IS NOT NULL AND "
 										"oid >= %u;",
 										ClanguageId,
 										FirstNormalObjectId);
-		totaltups += PQntuples(ress[dbnum]);
 
-		PQfinish(conn);
-	}
+	async_task_add_step(task, loadable_libraries_query,
+						get_loadable_libraries_result, false, &state);
+
+	async_task_run(task, &old_cluster);
+	async_task_free(task);
 
 	/*
 	 * Allocate memory for required libraries and logical replication output
 	 * plugins.
 	 */
-	n_libinfos = totaltups + count_old_cluster_logical_slots();
+	n_libinfos = state.totaltups + count_old_cluster_logical_slots();
 	os_info.libraries = (LibraryInfo *) pg_malloc(sizeof(LibraryInfo) * n_libinfos);
 	totaltups = 0;
 
 	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
 	{
-		PGresult   *res = ress[dbnum];
+		PGresult   *res = state.ress[dbnum];
 		int			ntups;
 		int			rowno;
 		LogicalSlotInfoArr *slot_arr = &old_cluster.dbarr.dbs[dbnum].slot_arr;
@@ -129,7 +137,8 @@ get_loadable_libraries(void)
 		}
 	}
 
-	pg_free(ress);
+	pg_free(state.ress);
+	pg_free(loadable_libraries_query);
 
 	os_info.num_libraries = totaltups;
 }
-- 
2.39.3 (Apple Git-146)

v9-0005-use-new-pg_upgrade-async-API-to-parallelize-repor.patchtext/plain; charset=us-asciiDownload
From 07a53259d5d5e52b081194ec3a9604192fcf3407 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:31:57 -0500
Subject: [PATCH v9 05/11] use new pg_upgrade async API to parallelize
 reporting extension updates

---
 src/bin/pg_upgrade/version.c | 77 +++++++++++++++++-------------------
 1 file changed, 36 insertions(+), 41 deletions(-)

diff --git a/src/bin/pg_upgrade/version.c b/src/bin/pg_upgrade/version.c
index 2de6dffccd..dd2b0cd975 100644
--- a/src/bin/pg_upgrade/version.c
+++ b/src/bin/pg_upgrade/version.c
@@ -139,6 +139,34 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 		check_ok();
 }
 
+static void
+report_extension_updates_result(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_name = PQfnumber(res, "name");
+	char	   *output_path = "update_extensions.sql";
+	FILE	  **script = (FILE **) arg;
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL && (*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			PQExpBufferData connectbuf;
+
+			initPQExpBuffer(&connectbuf);
+			appendPsqlMetaConnect(&connectbuf, dbinfo->db_name);
+			fputs(connectbuf.data, *script);
+			termPQExpBuffer(&connectbuf);
+			db_used = true;
+		}
+		fprintf(*script, "ALTER EXTENSION %s UPDATE;\n",
+				quote_identifier(PQgetvalue(res, rowno, i_name)));
+	}
+}
+
 /*
  * report_extension_updates()
  *	Report extensions that should be updated.
@@ -146,53 +174,20 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 void
 report_extension_updates(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char	   *output_path = "update_extensions.sql";
+	AsyncTask  *task = async_task_create();
+	const char *query = "SELECT name "
+		"FROM pg_available_extensions "
+		"WHERE installed_version != default_version";
 
 	prep_status("Checking for extension updates");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_name;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* find extensions needing updates */
-		res = executeQueryOrDie(conn,
-								"SELECT name "
-								"FROM pg_available_extensions "
-								"WHERE installed_version != default_version"
-			);
+	async_task_add_step(task, query, report_extension_updates_result,
+						true, &script);
 
-		ntups = PQntuples(res);
-		i_name = PQfnumber(res, "name");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				PQExpBufferData connectbuf;
-
-				initPQExpBuffer(&connectbuf);
-				appendPsqlMetaConnect(&connectbuf, active_db->db_name);
-				fputs(connectbuf.data, script);
-				termPQExpBuffer(&connectbuf);
-				db_used = true;
-			}
-			fprintf(script, "ALTER EXTENSION %s UPDATE;\n",
-					quote_identifier(PQgetvalue(res, rowno, i_name)));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v9-0006-parallelize-data-type-checks-in-pg_upgrade.patchtext/plain; charset=us-asciiDownload
From d54d60a2d71bee556c6b9343beafe40fb5bc2f54 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Sat, 6 Jul 2024 21:06:31 -0500
Subject: [PATCH v9 06/11] parallelize data type checks in pg_upgrade

---
 src/bin/pg_upgrade/check.c | 333 +++++++++++++++++++------------------
 1 file changed, 173 insertions(+), 160 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 48f5a0c0e6..150c101d1c 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -314,6 +314,133 @@ static DataTypesUsageChecks data_types_usage_checks[] =
 	}
 };
 
+struct data_type_check_state
+{
+	DataTypesUsageChecks *check;
+	bool		result;
+	PQExpBuffer *report;
+};
+
+static char *
+data_type_check_query(int checknum)
+{
+	DataTypesUsageChecks *check = &data_types_usage_checks[checknum];
+
+	return psprintf("WITH RECURSIVE oids AS ( "
+	/* start with the type(s) returned by base_query */
+					"	%s "
+					"	UNION ALL "
+					"	SELECT * FROM ( "
+	/* inner WITH because we can only reference the CTE once */
+					"		WITH x AS (SELECT oid FROM oids) "
+	/* domains on any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
+					"			UNION ALL "
+	/* arrays over any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
+					"			UNION ALL "
+	/* composite types containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
+					"			WHERE t.typtype = 'c' AND "
+					"				  t.oid = c.reltype AND "
+					"				  c.oid = a.attrelid AND "
+					"				  NOT a.attisdropped AND "
+					"				  a.atttypid = x.oid "
+					"			UNION ALL "
+	/* ranges containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
+					"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
+					"	) foo "
+					") "
+	/* now look for stored columns of any such type */
+					"SELECT n.nspname, c.relname, a.attname "
+					"FROM	pg_catalog.pg_class c, "
+					"		pg_catalog.pg_namespace n, "
+					"		pg_catalog.pg_attribute a "
+					"WHERE	c.oid = a.attrelid AND "
+					"		NOT a.attisdropped AND "
+					"		a.atttypid IN (SELECT oid FROM oids) AND "
+					"		c.relkind IN ("
+					CppAsString2(RELKIND_RELATION) ", "
+					CppAsString2(RELKIND_MATVIEW) ", "
+					CppAsString2(RELKIND_INDEX) ") AND "
+					"		c.relnamespace = n.oid AND "
+	/* exclude possible orphaned temp tables */
+					"		n.nspname !~ '^pg_temp_' AND "
+					"		n.nspname !~ '^pg_toast_temp_' AND "
+	/* exclude system catalogs, too */
+					"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
+					check->base_query);
+}
+
+static void
+data_type_check_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct data_type_check_state *state = (struct data_type_check_state *) arg;
+	int			ntups = PQntuples(res);
+
+	if (ntups)
+	{
+		char		output_path[MAXPGPATH];
+		int			i_nspname;
+		int			i_relname;
+		int			i_attname;
+		FILE	   *script = NULL;
+		bool		db_used = false;
+
+		snprintf(output_path, sizeof(output_path), "%s/%s",
+				 log_opts.basedir,
+				 state->check->report_filename);
+
+		/*
+		 * Make sure we have a buffer to save reports to now that we found a
+		 * first failing check.
+		 */
+		if (*state->report == NULL)
+			*state->report = createPQExpBuffer();
+
+		/*
+		 * If this is the first time we see an error for the check in question
+		 * then print a status message of the failure.
+		 */
+		if (!state->result)
+		{
+			pg_log(PG_REPORT, "    failed check: %s", _(state->check->status));
+			appendPQExpBuffer(*state->report, "\n%s\n%s    %s\n",
+							  _(state->check->report_text),
+							  _("A list of the problem columns is in the file:"),
+							  output_path);
+		}
+		state->result = true;
+
+		i_nspname = PQfnumber(res, "nspname");
+		i_relname = PQfnumber(res, "relname");
+		i_attname = PQfnumber(res, "attname");
+
+		for (int rowno = 0; rowno < ntups; rowno++)
+		{
+			if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
+				pg_fatal("could not open file \"%s\": %m", output_path);
+
+			if (!db_used)
+			{
+				fprintf(script, "In database: %s\n", dbinfo->db_name);
+				db_used = true;
+			}
+			fprintf(script, "  %s.%s.%s\n",
+					PQgetvalue(res, rowno, i_nspname),
+					PQgetvalue(res, rowno, i_relname),
+					PQgetvalue(res, rowno, i_attname));
+		}
+
+		if (script)
+		{
+			fclose(script);
+			script = NULL;
+		}
+	}
+}
+
 /*
  * check_for_data_types_usage()
  *	Detect whether there are any stored columns depending on given type(s)
@@ -336,11 +463,12 @@ static DataTypesUsageChecks data_types_usage_checks[] =
 static void
 check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
 {
-	bool		found = false;
-	bool	   *results;
-	PQExpBufferData report;
 	DataTypesUsageChecks *tmp = checks;
 	int			n_data_types_usage_checks = 0;
+	AsyncTask  *task = async_task_create();
+	char	  **queries = NULL;
+	struct data_type_check_state *states;
+	PQExpBuffer report = NULL;
 
 	prep_status("Checking for data type usage");
 
@@ -352,176 +480,61 @@ check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
 	}
 
 	/* Prepare an array to store the results of checks in */
-	results = pg_malloc0(sizeof(bool) * n_data_types_usage_checks);
+	queries = pg_malloc0(sizeof(char *) * n_data_types_usage_checks);
+	states = pg_malloc0(sizeof(struct data_type_check_state) * n_data_types_usage_checks);
 
-	/*
-	 * Connect to each database in the cluster and run all defined checks
-	 * against that database before trying the next one.
-	 */
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int i = 0; i < n_data_types_usage_checks; i++)
 	{
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
+		DataTypesUsageChecks *check = &checks[i];
 
-		for (int checknum = 0; checknum < n_data_types_usage_checks; checknum++)
+		if (check->threshold_version == MANUAL_CHECK)
 		{
-			PGresult   *res;
-			int			ntups;
-			int			i_nspname;
-			int			i_relname;
-			int			i_attname;
-			FILE	   *script = NULL;
-			bool		db_used = false;
-			char		output_path[MAXPGPATH];
-			DataTypesUsageChecks *cur_check = &checks[checknum];
-
-			if (cur_check->threshold_version == MANUAL_CHECK)
-			{
-				Assert(cur_check->version_hook);
-
-				/*
-				 * Make sure that the check applies to the current cluster
-				 * version and skip if not. If no check hook has been defined
-				 * we run the check for all versions.
-				 */
-				if (!cur_check->version_hook(cluster))
-					continue;
-			}
-			else if (cur_check->threshold_version != ALL_VERSIONS)
-			{
-				if (GET_MAJOR_VERSION(cluster->major_version) > cur_check->threshold_version)
-					continue;
-			}
-			else
-				Assert(cur_check->threshold_version == ALL_VERSIONS);
-
-			snprintf(output_path, sizeof(output_path), "%s/%s",
-					 log_opts.basedir,
-					 cur_check->report_filename);
+			Assert(check->version_hook);
 
 			/*
-			 * The type(s) of interest might be wrapped in a domain, array,
-			 * composite, or range, and these container types can be nested
-			 * (to varying extents depending on server version, but that's not
-			 * of concern here).  To handle all these cases we need a
-			 * recursive CTE.
+			 * Make sure that the check applies to the current cluster version
+			 * and skip it if not.
 			 */
-			res = executeQueryOrDie(conn,
-									"WITH RECURSIVE oids AS ( "
-			/* start with the type(s) returned by base_query */
-									"	%s "
-									"	UNION ALL "
-									"	SELECT * FROM ( "
-			/* inner WITH because we can only reference the CTE once */
-									"		WITH x AS (SELECT oid FROM oids) "
-			/* domains on any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
-									"			UNION ALL "
-			/* arrays over any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
-									"			UNION ALL "
-			/* composite types containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
-									"			WHERE t.typtype = 'c' AND "
-									"				  t.oid = c.reltype AND "
-									"				  c.oid = a.attrelid AND "
-									"				  NOT a.attisdropped AND "
-									"				  a.atttypid = x.oid "
-									"			UNION ALL "
-			/* ranges containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
-									"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
-									"	) foo "
-									") "
-			/* now look for stored columns of any such type */
-									"SELECT n.nspname, c.relname, a.attname "
-									"FROM	pg_catalog.pg_class c, "
-									"		pg_catalog.pg_namespace n, "
-									"		pg_catalog.pg_attribute a "
-									"WHERE	c.oid = a.attrelid AND "
-									"		NOT a.attisdropped AND "
-									"		a.atttypid IN (SELECT oid FROM oids) AND "
-									"		c.relkind IN ("
-									CppAsString2(RELKIND_RELATION) ", "
-									CppAsString2(RELKIND_MATVIEW) ", "
-									CppAsString2(RELKIND_INDEX) ") AND "
-									"		c.relnamespace = n.oid AND "
-			/* exclude possible orphaned temp tables */
-									"		n.nspname !~ '^pg_temp_' AND "
-									"		n.nspname !~ '^pg_toast_temp_' AND "
-			/* exclude system catalogs, too */
-									"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
-									cur_check->base_query);
-
-			ntups = PQntuples(res);
+			if (!check->version_hook(cluster))
+				continue;
+		}
+		else if (check->threshold_version != ALL_VERSIONS)
+		{
+			if (GET_MAJOR_VERSION(cluster->major_version) > check->threshold_version)
+				continue;
+		}
+		else
+			Assert(check->threshold_version == ALL_VERSIONS);
 
-			/*
-			 * The datatype was found, so extract the data and log to the
-			 * requested filename. We need to open the file for appending
-			 * since the check might have already found the type in another
-			 * database earlier in the loop.
-			 */
-			if (ntups)
-			{
-				/*
-				 * Make sure we have a buffer to save reports to now that we
-				 * found a first failing check.
-				 */
-				if (!found)
-					initPQExpBuffer(&report);
-				found = true;
-
-				/*
-				 * If this is the first time we see an error for the check in
-				 * question then print a status message of the failure.
-				 */
-				if (!results[checknum])
-				{
-					pg_log(PG_REPORT, "    failed check: %s", _(cur_check->status));
-					appendPQExpBuffer(&report, "\n%s\n%s    %s\n",
-									  _(cur_check->report_text),
-									  _("A list of the problem columns is in the file:"),
-									  output_path);
-				}
-				results[checknum] = true;
-
-				i_nspname = PQfnumber(res, "nspname");
-				i_relname = PQfnumber(res, "relname");
-				i_attname = PQfnumber(res, "attname");
-
-				for (int rowno = 0; rowno < ntups; rowno++)
-				{
-					if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
-						pg_fatal("could not open file \"%s\": %m", output_path);
-
-					if (!db_used)
-					{
-						fprintf(script, "In database: %s\n", active_db->db_name);
-						db_used = true;
-					}
-					fprintf(script, "  %s.%s.%s\n",
-							PQgetvalue(res, rowno, i_nspname),
-							PQgetvalue(res, rowno, i_relname),
-							PQgetvalue(res, rowno, i_attname));
-				}
-
-				if (script)
-				{
-					fclose(script);
-					script = NULL;
-				}
-			}
+		queries[i] = data_type_check_query(i);
 
-			PQclear(res);
-		}
+		states[i].check = &data_types_usage_checks[i];
+		states[i].report = &report;
 
-		PQfinish(conn);
+		async_task_add_step(task, queries[i],
+							data_type_check_process, true, &states[i]);
 	}
 
-	if (found)
-		pg_fatal("Data type checks failed: %s", report.data);
+	/*
+	 * Connect to each database in the cluster and run all defined checks
+	 * against that database before trying the next one.
+	 */
+	async_task_run(task, cluster);
+	async_task_free(task);
 
-	pg_free(results);
+	if (report)
+	{
+		pg_fatal("Data type checks failed: %s", report->data);
+		destroyPQExpBuffer(report);
+	}
+
+	for (int i = 0; i < n_data_types_usage_checks; i++)
+	{
+		if (queries[i])
+			pg_free(queries[i]);
+	}
+	pg_free(queries);
+	pg_free(states);
 
 	check_ok();
 }
-- 
2.39.3 (Apple Git-146)

v9-0007-parallelize-isn-and-int8-passing-mismatch-check-i.patchtext/plain; charset=us-asciiDownload
From 8d0580f5b64639a8b691ee33466294518502f8f9 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:00:20 -0500
Subject: [PATCH v9 07/11] parallelize isn and int8 passing mismatch check in
 pg_upgrade

---
 src/bin/pg_upgrade/check.c | 80 +++++++++++++++++++-------------------
 1 file changed, 39 insertions(+), 41 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 150c101d1c..5642404a2f 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1207,6 +1207,34 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 	check_ok();
 }
 
+static void
+isn_and_int8_passing_mismatch_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_proname = PQfnumber(res, "proname");
+	FILE	  **script = (FILE **) arg;
+	char		output_path[MAXPGPATH];
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "contrib_isn_and_int8_pass_by_value.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL && (*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_proname));
+	}
+}
 
 /*
  *	check_for_isn_and_int8_passing_mismatch()
@@ -1218,9 +1246,14 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 static void
 check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
+	AsyncTask  *task;
 	char		output_path[MAXPGPATH];
+	const char *query = "SELECT n.nspname, p.proname "
+		"FROM   pg_catalog.pg_proc p, "
+		"       pg_catalog.pg_namespace n "
+		"WHERE  p.pronamespace = n.oid AND "
+		"       p.probin = '$libdir/isn'";
 
 	prep_status("Checking for contrib/isn with bigint-passing mismatch");
 
@@ -1236,46 +1269,11 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "contrib_isn_and_int8_pass_by_value.txt");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_proname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* Find any functions coming from contrib/isn */
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, p.proname "
-								"FROM	pg_catalog.pg_proc p, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	p.pronamespace = n.oid AND "
-								"		p.probin = '$libdir/isn'");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_proname = PQfnumber(res, "proname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_proname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	task = async_task_create();
+	async_task_add_step(task, query,
+						isn_and_int8_passing_mismatch_process, true, &script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v9-0008-parallelize-user-defined-postfix-ops-check-in-pg_.patchtext/plain; charset=us-asciiDownload
From 095269b37b48ff76f3fb0065a99a7f78be0f24a3 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:12:49 -0500
Subject: [PATCH v9 08/11] parallelize user defined postfix ops check in
 pg_upgrade

---
 src/bin/pg_upgrade/check.c | 131 +++++++++++++++++++------------------
 1 file changed, 66 insertions(+), 65 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 5642404a2f..25cd10c000 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1291,15 +1291,76 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 		check_ok();
 }
 
+static void
+user_defined_postfix_ops_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	FILE	  **script = (FILE **) arg;
+	char		output_path[MAXPGPATH];
+	int			ntups = PQntuples(res);
+	bool		db_used = false;
+	int			i_oproid = PQfnumber(res, "oproid");
+	int			i_oprnsp = PQfnumber(res, "oprnsp");
+	int			i_oprname = PQfnumber(res, "oprname");
+	int			i_typnsp = PQfnumber(res, "typnsp");
+	int			i_typname = PQfnumber(res, "typname");
+
+	if (!ntups)
+		return;
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "postfix_ops.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL &&
+			(*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
+				PQgetvalue(res, rowno, i_oproid),
+				PQgetvalue(res, rowno, i_oprnsp),
+				PQgetvalue(res, rowno, i_oprname),
+				PQgetvalue(res, rowno, i_typnsp),
+				PQgetvalue(res, rowno, i_typname));
+	}
+}
+
 /*
  * Verify that no user defined postfix operators exist.
  */
 static void
 check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
+	const char *query;
+
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	query = "SELECT o.oid AS oproid, "
+		"       n.nspname AS oprnsp, "
+		"       o.oprname, "
+		"       tn.nspname AS typnsp, "
+		"       t.typname "
+		"FROM pg_catalog.pg_operator o, "
+		"     pg_catalog.pg_namespace n, "
+		"     pg_catalog.pg_type t, "
+		"     pg_catalog.pg_namespace tn "
+		"WHERE o.oprnamespace = n.oid AND "
+		"      o.oprleft = t.oid AND "
+		"      t.typnamespace = tn.oid AND "
+		"      o.oprright = 0 AND "
+		"      o.oid >= 16384";
 
 	prep_status("Checking for user-defined postfix operators");
 
@@ -1307,70 +1368,10 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "postfix_ops.txt");
 
-	/* Find any user defined postfix operators */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_oproid,
-					i_oprnsp,
-					i_oprname,
-					i_typnsp,
-					i_typname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT o.oid AS oproid, "
-								"       n.nspname AS oprnsp, "
-								"       o.oprname, "
-								"       tn.nspname AS typnsp, "
-								"       t.typname "
-								"FROM pg_catalog.pg_operator o, "
-								"     pg_catalog.pg_namespace n, "
-								"     pg_catalog.pg_type t, "
-								"     pg_catalog.pg_namespace tn "
-								"WHERE o.oprnamespace = n.oid AND "
-								"      o.oprleft = t.oid AND "
-								"      t.typnamespace = tn.oid AND "
-								"      o.oprright = 0 AND "
-								"      o.oid >= 16384");
-		ntups = PQntuples(res);
-		i_oproid = PQfnumber(res, "oproid");
-		i_oprnsp = PQfnumber(res, "oprnsp");
-		i_oprname = PQfnumber(res, "oprname");
-		i_typnsp = PQfnumber(res, "typnsp");
-		i_typname = PQfnumber(res, "typname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
-					PQgetvalue(res, rowno, i_oproid),
-					PQgetvalue(res, rowno, i_oprnsp),
-					PQgetvalue(res, rowno, i_oprname),
-					PQgetvalue(res, rowno, i_typnsp),
-					PQgetvalue(res, rowno, i_typname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_add_step(task, query,
+						user_defined_postfix_ops_process, true, &script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v9-0009-parallelize-incompatible-polymorphics-check-in-pg.patchtext/plain; charset=us-asciiDownload
From 3c3b6bd7463d56de507da67fda62d241bb0a3a1a Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:30:19 -0500
Subject: [PATCH v9 09/11] parallelize incompatible polymorphics check in
 pg_upgrade

---
 src/bin/pg_upgrade/check.c       | 155 ++++++++++++++++---------------
 src/tools/pgindent/typedefs.list |   1 +
 2 files changed, 81 insertions(+), 75 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 25cd10c000..af7d093581 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1387,6 +1387,38 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 		check_ok();
 }
 
+typedef struct incompat_polymorphics_state
+{
+	FILE	   *script;
+	char		output_path[MAXPGPATH];
+} incompat_polymorphics_state;
+
+static void
+incompat_polymorphics_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	incompat_polymorphics_state *state = (incompat_polymorphics_state *) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_objkind = PQfnumber(res, "objkind");
+	int			i_objname = PQfnumber(res, "objname");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (state->script == NULL &&
+			(state->script = fopen_priv(state->output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", state->output_path);
+		if (!db_used)
+		{
+			fprintf(state->script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+
+		fprintf(state->script, "  %s: %s\n",
+				PQgetvalue(res, rowno, i_objkind),
+				PQgetvalue(res, rowno, i_objname));
+	}
+}
+
 /*
  *	check_for_incompatible_polymorphics()
  *
@@ -1396,14 +1428,15 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 static void
 check_for_incompatible_polymorphics(ClusterInfo *cluster)
 {
-	PGresult   *res;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
 	PQExpBufferData old_polymorphics;
+	AsyncTask  *task = async_task_create();
+	incompat_polymorphics_state state;
+	char	   *query;
 
 	prep_status("Checking for incompatible polymorphic functions");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	state.script = NULL;
+	snprintf(state.output_path, sizeof(state.output_path), "%s/%s",
 			 log_opts.basedir,
 			 "incompatible_polymorphics.txt");
 
@@ -1427,80 +1460,51 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 							 ", 'array_positions(anyarray,anyelement)'"
 							 ", 'width_bucket(anyelement,anyarray)'");
 
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		bool		db_used = false;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-		int			ntups;
-		int			i_objkind,
-					i_objname;
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-		/* Aggregate transition functions */
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Aggregate final functions */
-								"UNION ALL "
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Operators */
-								"UNION ALL "
-								"SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
-								"FROM pg_operator AS op "
-								"WHERE op.oid >= 16384 "
-								"AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
-								old_polymorphics.data,
-								old_polymorphics.data,
-								old_polymorphics.data);
-
-		ntups = PQntuples(res);
-
-		i_objkind = PQfnumber(res, "objkind");
-		i_objname = PQfnumber(res, "objname");
-
-		for (int rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
 
-			fprintf(script, "  %s: %s\n",
-					PQgetvalue(res, rowno, i_objkind),
-					PQgetvalue(res, rowno, i_objname));
-		}
+	/* Aggregate transition functions */
+	query = psprintf("SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					 "FROM pg_proc AS p "
+					 "JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					 "JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
+					 "WHERE p.oid >= 16384 "
+					 "AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Aggregate final functions */
+					 "UNION ALL "
+					 "SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					 "FROM pg_proc AS p "
+					 "JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					 "JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
+					 "WHERE p.oid >= 16384 "
+					 "AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Operators */
+					 "UNION ALL "
+					 "SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
+					 "FROM pg_operator AS op "
+					 "WHERE op.oid >= 16384 "
+					 "AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
+					 old_polymorphics.data,
+					 old_polymorphics.data,
+					 old_polymorphics.data);
 
-		PQclear(res);
-		PQfinish(conn);
-	}
+	async_task_add_step(task, query,
+						incompat_polymorphics_process, true, &state);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
-	if (script)
+	if (state.script)
 	{
-		fclose(script);
+		fclose(state.script);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains user-defined objects that refer to internal\n"
 				 "polymorphic functions with arguments of type \"anyarray\" or \"anyelement\".\n"
@@ -1508,12 +1512,13 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 				 "afterwards, changing them to refer to the new corresponding functions with\n"
 				 "arguments of type \"anycompatiblearray\" and \"anycompatible\".\n"
 				 "A list of the problematic objects is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", state.output_path);
 	}
 	else
 		check_ok();
 
 	termPQExpBuffer(&old_polymorphics);
+	pg_free(query);
 }
 
 /*
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ec8106329d..03be80931e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3545,6 +3545,7 @@ hstoreUpgrade_t
 hyperLogLogState
 ifState
 import_error_callback_arg
+incompat_polymorphics_state
 indexed_tlist
 inet
 inetKEY
-- 
2.39.3 (Apple Git-146)

v9-0010-parallelize-tables-with-oids-check-in-pg_upgrade.patchtext/plain; charset=us-asciiDownload
From 3f0bc8b9fdb1b7f664fb3ae546a4664b271378ce Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:42:22 -0500
Subject: [PATCH v9 10/11] parallelize tables with oids check in pg_upgrade

---
 src/bin/pg_upgrade/check.c | 85 +++++++++++++++++++-------------------
 1 file changed, 43 insertions(+), 42 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index af7d093581..4156257843 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1521,15 +1521,53 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 	pg_free(query);
 }
 
+static void
+with_oids_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	char		output_path[MAXPGPATH];
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+	FILE	  **script = (FILE **) arg;
+
+	if (!ntups)
+		return;
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "tables_with_oids.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL && (*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_relname));
+	}
+}
+
 /*
  * Verify that no tables are declared WITH OIDS.
  */
 static void
 check_for_tables_with_oids(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
+	const char *query = "SELECT n.nspname, c.relname "
+		"FROM   pg_catalog.pg_class c, "
+		"       pg_catalog.pg_namespace n "
+		"WHERE  c.relnamespace = n.oid AND "
+		"       c.relhasoids AND"
+		"       n.nspname NOT IN ('pg_catalog')";
 
 	prep_status("Checking for tables WITH OIDS");
 
@@ -1537,47 +1575,10 @@ check_for_tables_with_oids(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "tables_with_oids.txt");
 
-	/* Find any tables declared WITH OIDS */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_relname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, c.relname "
-								"FROM	pg_catalog.pg_class c, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	c.relnamespace = n.oid AND "
-								"		c.relhasoids AND"
-								"       n.nspname NOT IN ('pg_catalog')");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_relname = PQfnumber(res, "relname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_relname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	async_task_add_step(task, query,
+						with_oids_process, true, &script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

v9-0011-parallelize-user-defined-encoding-conversions-che.patchtext/plain; charset=us-asciiDownload
From 7a665465afe5fbe027df70467d518ddc235c33b7 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:52:13 -0500
Subject: [PATCH v9 11/11] parallelize user defined encoding conversions check
 in pg_upgrade

---
 src/bin/pg_upgrade/check.c | 102 +++++++++++++++++++------------------
 1 file changed, 53 insertions(+), 49 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 4156257843..6910d18713 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1652,15 +1652,51 @@ check_for_pg_role_prefix(ClusterInfo *cluster)
 		check_ok();
 }
 
+static void
+user_defined_encoding_conversions_process(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	FILE	  **script = (FILE **) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	char		output_path[MAXPGPATH];
+	int			i_conoid = PQfnumber(res, "conoid");
+	int			i_conname = PQfnumber(res, "conname");
+	int			i_nspname = PQfnumber(res, "nspname");
+
+	if (!ntups)
+		return;
+
+	snprintf(output_path, sizeof(output_path), "%s/%s",
+			 log_opts.basedir,
+			 "encoding_conversions.txt");
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (*script == NULL &&
+			(*script = fopen_priv(output_path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", output_path);
+		if (!db_used)
+		{
+			fprintf(*script, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(*script, "  (oid=%s) %s.%s\n",
+				PQgetvalue(res, rowno, i_conoid),
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_conname));
+	}
+}
+
 /*
  * Verify that no user-defined encoding conversions exist.
  */
 static void
 check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
 {
-	int			dbnum;
 	FILE	   *script = NULL;
 	char		output_path[MAXPGPATH];
+	AsyncTask  *task = async_task_create();
+	const char *query;
 
 	prep_status("Checking for user-defined encoding conversions");
 
@@ -1668,55 +1704,23 @@ check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
 			 log_opts.basedir,
 			 "encoding_conversions.txt");
 
-	/* Find any user defined encoding conversions */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_conoid,
-					i_conname,
-					i_nspname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT c.oid as conoid, c.conname, n.nspname "
-								"FROM pg_catalog.pg_conversion c, "
-								"     pg_catalog.pg_namespace n "
-								"WHERE c.connamespace = n.oid AND "
-								"      c.oid >= 16384");
-		ntups = PQntuples(res);
-		i_conoid = PQfnumber(res, "conoid");
-		i_conname = PQfnumber(res, "conname");
-		i_nspname = PQfnumber(res, "nspname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s\n",
-					PQgetvalue(res, rowno, i_conoid),
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_conname));
-		}
-
-		PQclear(res);
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	query = "SELECT c.oid as conoid, c.conname, n.nspname "
+		"FROM pg_catalog.pg_conversion c, "
+		"     pg_catalog.pg_namespace n "
+		"WHERE c.connamespace = n.oid AND "
+		"      c.oid >= 16384";
 
-		PQfinish(conn);
-	}
+	async_task_add_step(task, query,
+						user_defined_encoding_conversions_process, true,
+						&script);
+	async_task_run(task, cluster);
+	async_task_free(task);
 
 	if (script)
 	{
-- 
2.39.3 (Apple Git-146)

#28Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#27)
11 attachment(s)
Re: optimizing pg_upgrade's once-in-each-database steps

I spent some time cleaning up names, comments, etc. Barring additional
feedback, I'm planning to commit this stuff in the September commitfest so
that it has plenty of time to bake in the buildfarm.

--
nathan

Attachments:

v10-0001-Introduce-framework-for-parallelizing-various-pg.patchtext/plain; charset=us-asciiDownload
From c38137cd2cbc45c348e8e144f864a62c08bd7b7f Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 10:45:59 -0500
Subject: [PATCH v10 01/11] Introduce framework for parallelizing various
 pg_upgrade tasks.

A number of pg_upgrade steps require connecting to every database
in the cluster and running the same query in each one.  When there
are many databases, these steps are particularly time-consuming,
especially since these steps are performed sequentially in a single
process.

This commit introduces a new framework that makes it easy to
parallelize most of these once-in-each-database tasks.
Specifically, it manages a simple state machine of slots and uses
libpq's asynchronous APIs to establish the connections and run the
queries.  The --jobs option is used to determine the number of
slots to use.  To use this new task framework, callers simply need
to provide the query and a callback function to process its
results, and the framework takes care of the rest.  A more complete
description is provided at the top of the new task.c file.

None of the eligible once-in-each-database tasks are converted to
use this new framework in this commit.  That will be done via
several follow-up commits.

Reviewed-by: Jeff Davis, Robert Haas, Daniel Gustafsson, Ilya Gladyshev, Corey Huinker
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/Makefile      |   1 +
 src/bin/pg_upgrade/meson.build   |   1 +
 src/bin/pg_upgrade/pg_upgrade.h  |  21 ++
 src/bin/pg_upgrade/task.c        | 426 +++++++++++++++++++++++++++++++
 src/tools/pgindent/typedefs.list |   5 +
 5 files changed, 454 insertions(+)
 create mode 100644 src/bin/pg_upgrade/task.c

diff --git a/src/bin/pg_upgrade/Makefile b/src/bin/pg_upgrade/Makefile
index bde91e2beb..f83d2b5d30 100644
--- a/src/bin/pg_upgrade/Makefile
+++ b/src/bin/pg_upgrade/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	relfilenumber.o \
 	server.o \
 	tablespace.o \
+	task.o \
 	util.o \
 	version.o
 
diff --git a/src/bin/pg_upgrade/meson.build b/src/bin/pg_upgrade/meson.build
index 9825fa3305..3d88419674 100644
--- a/src/bin/pg_upgrade/meson.build
+++ b/src/bin/pg_upgrade/meson.build
@@ -14,6 +14,7 @@ pg_upgrade_sources = files(
   'relfilenumber.c',
   'server.c',
   'tablespace.c',
+  'task.c',
   'util.c',
   'version.c',
 )
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index cdb6e2b759..53f693c2d4 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -494,3 +494,24 @@ void		parallel_transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr
 										  char *old_pgdata, char *new_pgdata,
 										  char *old_tablespace);
 bool		reap_child(bool wait_for_child);
+
+/* task.c */
+
+typedef void (*UpgradeTaskProcessCB) (DbInfo *dbinfo, PGresult *res, void *arg);
+
+/* struct definition is private to task.c */
+typedef struct UpgradeTask UpgradeTask;
+
+UpgradeTask *upgrade_task_create(void);
+void		upgrade_task_add_step(UpgradeTask *task, const char *query,
+								  UpgradeTaskProcessCB process_cb, bool free_result,
+								  void *arg);
+void		upgrade_task_run(const UpgradeTask *task, const ClusterInfo *cluster);
+void		upgrade_task_free(UpgradeTask *task);
+
+/* convenient type for common private data needed by several tasks */
+typedef struct
+{
+	FILE	   *file;
+	char		path[MAXPGPATH];
+} UpgradeTaskReport;
diff --git a/src/bin/pg_upgrade/task.c b/src/bin/pg_upgrade/task.c
new file mode 100644
index 0000000000..b5e2455ae2
--- /dev/null
+++ b/src/bin/pg_upgrade/task.c
@@ -0,0 +1,426 @@
+/*
+ * task.c
+ *		framework for parallelizing pg_upgrade's once-in-each-database tasks
+ *
+ * This framework provides an efficient way of running the various
+ * once-in-each-database tasks required by pg_upgrade.  Specifically, it
+ * parallelizes these tasks by managing a simple state machine of
+ * user_opts.jobs slots and using libpq's asynchronous APIs to establish the
+ * connections and run the queries.  Callers simply need to create a callback
+ * function and build/execute an UpgradeTask.  A simple example follows:
+ *
+ *		static void
+ *		my_process_cb(DbInfo *dbinfo, PGresult *res, void *arg)
+ *		{
+ *			for (int i = 0; i < PQntuples(res); i++)
+ *			{
+ *				... process results ...
+ *			}
+ *		}
+ *
+ *		void
+ *		my_task(ClusterInfo *cluster)
+ *		{
+ *			UpgradeTask *task = upgrade_task_create();
+ *
+ *			upgrade_task_add_step(task,
+ *								  "... query text ...",
+ *								  my_process_cb,
+ *								  true,		// let the task free the PGresult
+ *								  NULL);	// "arg" pointer for callback
+ *			upgrade_task_run(task, cluster);
+ *			upgrade_task_free(task);
+ *		}
+ *
+ * Note that multiple steps can be added to a given task.  When there are
+ * multiple steps, the task will run all of the steps consecutively in the same
+ * database connection before freeing the connection and moving on.  In other
+ * words, it only ever initiates one connection to each database in the
+ * cluster for a given run.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ * src/bin/pg_upgrade/task.c
+ */
+
+#include "postgres_fe.h"
+
+#include "common/connect.h"
+#include "fe_utils/string_utils.h"
+#include "pg_upgrade.h"
+
+/*
+ * dbs_complete stores the number of databases that we have completed
+ * processing.  When this value equals the number of databases in the cluster,
+ * the task is finished.
+ */
+static int	dbs_complete;
+
+/*
+ * dbs_processing stores the index of the next database in the cluster's array
+ * of databases that will be picked up for processing.  It will always be
+ * greater than or equal to dbs_complete.
+ */
+static int	dbs_processing;
+
+/*
+ * This struct stores all the information for a single step of a task.  All
+ * steps in a task are run in a single connection before moving on to the next
+ * database (which requires a new connection).
+ */
+typedef struct UpgradeTaskStep
+{
+	UpgradeTaskProcessCB process_cb;	/* processes the results of the query */
+	const char *query;			/* query text */
+	bool		free_result;	/* should we free the result? */
+	void	   *arg;			/* pointer passed to process_cb */
+} UpgradeTaskStep;
+
+/*
+ * This struct is a thin wrapper around an array of steps, i.e.,
+ * UpgradeTaskStep.
+ */
+typedef struct UpgradeTask
+{
+	UpgradeTaskStep *steps;
+	int			num_steps;
+} UpgradeTask;
+
+/*
+ * The different states for a parallel slot.
+ */
+typedef enum
+{
+	FREE,						/* slot available for use in a new database */
+	CONNECTING,					/* waiting for connection to be established */
+	RUNNING_QUERIES,			/* running/processing queries in the task */
+} UpgradeTaskSlotState;
+
+/*
+ * We maintain an array of user_opts.jobs slots to execute the task.
+ */
+typedef struct
+{
+	UpgradeTaskSlotState state; /* state of the slot */
+	int			db_idx;			/* index of the database assigned to slot */
+	int			step_idx;		/* index of the current step of task */
+	PGconn	   *conn;			/* current connection managed by slot */
+} UpgradeTaskSlot;
+
+/*
+ * Initializes an UpgradeTask.
+ */
+UpgradeTask *
+upgrade_task_create(void)
+{
+	UpgradeTask *task = pg_malloc0(sizeof(UpgradeTask));
+
+	/* All tasks must first set a secure search_path. */
+	upgrade_task_add_step(task, ALWAYS_SECURE_SEARCH_PATH_SQL, NULL, true, NULL);
+	return task;
+}
+
+/*
+ * Frees all storage associated with an UpgradeTask.
+ */
+void
+upgrade_task_free(UpgradeTask *task)
+{
+	if (task->steps)
+		pg_free(task->steps);
+
+	pg_free(task);
+}
+
+/*
+ * Adds a step to an UpgradeTask.  The steps will be executed in each database
+ * in the order in which they are added.
+ *
+ *	task: task object that must have been initialized via upgrade_task_create()
+ *	query: the query text
+ *	process_cb: function that processes the results of the query
+ *	free_result: should we free the PGresult, or leave it to the caller?
+ *	arg: pointer to task-specific data that is passed to each callback
+ */
+void
+upgrade_task_add_step(UpgradeTask *task, const char *query,
+					  UpgradeTaskProcessCB process_cb, bool free_result,
+					  void *arg)
+{
+	UpgradeTaskStep *new_step;
+
+	task->steps = pg_realloc(task->steps,
+							 ++task->num_steps * sizeof(UpgradeTaskStep));
+
+	new_step = &task->steps[task->num_steps - 1];
+	new_step->process_cb = process_cb;
+	new_step->query = query;
+	new_step->free_result = free_result;
+	new_step->arg = arg;
+}
+
+/*
+ * Build a connection string for the slot's current database and asynchronously
+ * start a new connection, but do not wait for the connection to be
+ * established.
+ */
+static void
+start_conn(const ClusterInfo *cluster, UpgradeTaskSlot *slot)
+{
+	PQExpBufferData conn_opts;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db_idx];
+
+	/* Build connection string with proper quoting */
+	initPQExpBuffer(&conn_opts);
+	appendPQExpBufferStr(&conn_opts, "dbname=");
+	appendConnStrVal(&conn_opts, dbinfo->db_name);
+	appendPQExpBufferStr(&conn_opts, " user=");
+	appendConnStrVal(&conn_opts, os_info.user);
+	appendPQExpBuffer(&conn_opts, " port=%d", cluster->port);
+	if (cluster->sockdir)
+	{
+		appendPQExpBufferStr(&conn_opts, " host=");
+		appendConnStrVal(&conn_opts, cluster->sockdir);
+	}
+
+	slot->conn = PQconnectStart(conn_opts.data);
+
+	if (!slot->conn)
+		pg_fatal("failed to create connection with connection string: \"%s\"",
+				 conn_opts.data);
+
+	termPQExpBuffer(&conn_opts);
+}
+
+/*
+ * Run the process_cb callback function to process the result of a query, and
+ * free the result if the caller indicated we should do so.
+ */
+static void
+process_query_result(const ClusterInfo *cluster, UpgradeTaskSlot *slot,
+					 const UpgradeTask *task)
+{
+	UpgradeTaskStep *steps = &task->steps[slot->step_idx];
+	UpgradeTaskProcessCB process_cb = steps->process_cb;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db_idx];
+	PGresult   *res = PQgetResult(slot->conn);
+
+	if (PQstatus(slot->conn) == CONNECTION_BAD ||
+		(PQresultStatus(res) != PGRES_TUPLES_OK &&
+		 PQresultStatus(res) != PGRES_COMMAND_OK))
+		pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+
+	/*
+	 * We assume that a NULL process_cb callback function means there's
+	 * nothing to process.  This is primarily intended for the inital step in
+	 * every task that sets a safe search_path.
+	 */
+	if (process_cb)
+		(*process_cb) (dbinfo, res, steps->arg);
+
+	if (steps->free_result)
+		PQclear(res);
+}
+
+/*
+ * Advances the state machine for a given slot as necessary.
+ */
+static void
+process_slot(const ClusterInfo *cluster, UpgradeTaskSlot *slot, const UpgradeTask *task)
+{
+	PQExpBufferData queries;
+
+	switch (slot->state)
+	{
+		case FREE:
+
+			/*
+			 * If all of the databases in the cluster have been processed or
+			 * are currently being processed by other slots, we are done.
+			 */
+			if (dbs_processing >= cluster->dbarr.ndbs)
+				return;
+
+			/*
+			 * Claim the next database in the cluster's array and initiate a
+			 * new connection.
+			 */
+			slot->db_idx = dbs_processing++;
+			slot->state = CONNECTING;
+			start_conn(cluster, slot);
+
+			return;
+
+		case CONNECTING:
+
+			/* Check for connection failure. */
+			if (PQconnectPoll(slot->conn) == PGRES_POLLING_FAILED)
+				pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+
+			/* Check whether the connection is still establishing. */
+			if (PQconnectPoll(slot->conn) != PGRES_POLLING_OK)
+				return;
+
+			/*
+			 * Move on to running/processing the queries in the task.  We
+			 * combine all the queries and send them to the server together.
+			 */
+			slot->state = RUNNING_QUERIES;
+			initPQExpBuffer(&queries);
+			for (int i = 0; i < task->num_steps; i++)
+				appendPQExpBuffer(&queries, "%s;", task->steps[i].query);
+			if (!PQsendQuery(slot->conn, queries.data))
+				pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+			termPQExpBuffer(&queries);
+
+			return;
+
+		case RUNNING_QUERIES:
+
+			/*
+			 * Consume any available data and clear the read-ready indicator
+			 * for the connection.
+			 */
+			if (!PQconsumeInput(slot->conn))
+				pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+
+			/*
+			 * Process any results that are ready so that we can free up this
+			 * slot for another database as soon as possible.
+			 */
+			for (; slot->step_idx < task->num_steps; slot->step_idx++)
+			{
+				/* If no more results are available yet, move on. */
+				if (PQisBusy(slot->conn))
+					return;
+
+				process_query_result(cluster, slot, task);
+			}
+
+			/*
+			 * If we just finished processing the result of the last step in
+			 * the task, free the slot.  We recursively call this function on
+			 * the newly-freed slot so that we can start initiating the next
+			 * connection immediately instead of waiting for the next loop
+			 * through the slots.
+			 */
+			dbs_complete++;
+			(void) PQgetResult(slot->conn);
+			PQfinish(slot->conn);
+			memset(slot, 0, sizeof(UpgradeTaskSlot));
+
+			process_slot(cluster, slot, task);
+
+			return;
+	}
+}
+
+/*
+ * Wait on the slots to either finish connecting or to receive query results if
+ * possible.  This avoids a tight loop in upgrade_task_run().
+ */
+static void
+wait_on_slots(UpgradeTaskSlot *slots, int numslots)
+{
+	fd_set		input_mask;
+	fd_set		output_mask;
+	fd_set		except_mask;
+	int			maxFd = 0;
+
+	FD_ZERO(&input_mask);
+	FD_ZERO(&output_mask);
+	FD_ZERO(&except_mask);
+
+	for (int i = 0; i < numslots; i++)
+	{
+		int			sock;
+		bool		read = false;
+
+		switch (slots[i].state)
+		{
+			case FREE:
+
+				/*
+				 * This function should only ever see free slots as we are
+				 * finishing processing the last few databases, at which point
+				 * we don't have any databases left for them to process. We'll
+				 * never use these slots again, so we can safely ignore them.
+				 */
+				continue;
+
+			case CONNECTING:
+
+				/*
+				 * If we are waiting for the connection to establish, choose
+				 * whether to wait for reading or for writing on the socket as
+				 * appropriate.  If neither apply, just return immediately so
+				 * that we can handle the slot.
+				 */
+				{
+					PostgresPollingStatusType status;
+
+					status = PQconnectPoll(slots[i].conn);
+					if (status == PGRES_POLLING_READING)
+						read = true;
+					else if (status != PGRES_POLLING_WRITING)
+						return;
+				}
+				break;
+
+			case RUNNING_QUERIES:
+
+				/*
+				 * Once we've sent the queries, we must wait for the socket to
+				 * be read-ready.  Note that process_slot() handles calling
+				 * PQconsumeInput() as required.
+				 */
+				read = true;
+				break;
+		}
+
+		/*
+		 * If there's some problem retrieving the socket, just pretend this
+		 * slot doesn't exist.  We don't expect this to happen regularly in
+		 * practice, so it seems unlikely to cause too much harm.
+		 */
+		sock = PQsocket(slots[i].conn);
+		if (sock < 0)
+			continue;
+
+		/*
+		 * Add the socket to the set.
+		 */
+		FD_SET(sock, read ? &input_mask : &output_mask);
+		FD_SET(sock, &except_mask);
+		maxFd = Max(maxFd, sock);
+	}
+
+	/*
+	 * If we found socket(s) to wait on, wait.
+	 */
+	if (maxFd != 0)
+		(void) select(maxFd + 1, &input_mask, &output_mask, &except_mask, NULL);
+}
+
+/*
+ * Runs all the steps of the task in every database in the cluster using
+ * user_opts.jobs parallel slots.
+ */
+void
+upgrade_task_run(const UpgradeTask *task, const ClusterInfo *cluster)
+{
+	int			jobs = Max(1, user_opts.jobs);
+	UpgradeTaskSlot *slots = pg_malloc0(sizeof(UpgradeTaskSlot) * jobs);
+
+	dbs_complete = 0;
+	dbs_processing = 0;
+
+	while (dbs_complete < cluster->dbarr.ndbs)
+	{
+		for (int i = 0; i < jobs; i++)
+			process_slot(cluster, &slots[i], task);
+
+		wait_on_slots(slots, jobs);
+	}
+
+	pg_free(slots);
+}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9e951a9e6f..43c0f9f85b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3039,6 +3039,11 @@ UnresolvedTup
 UnresolvedTupData
 UpdateContext
 UpdateStmt
+UpgradeTask
+UpgradeTaskReport
+UpgradeTaskSlot
+UpgradeTaskSlotState
+UpgradeTaskStep
 UploadManifestCmd
 UpperRelationKind
 UpperUniquePath
-- 
2.39.3 (Apple Git-146)

v10-0002-Use-pg_upgrade-s-new-parallel-framework-for-subs.patchtext/plain; charset=us-asciiDownload
From a1f2ff2ea0a6f9e0eb9af0ec1e400dbc048079cb Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 17:21:19 -0500
Subject: [PATCH v10 02/11] Use pg_upgrade's new parallel framework for
 subscription checks.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 206 ++++++++++++++++++++-----------------
 1 file changed, 111 insertions(+), 95 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 96adea41e9..f8160e0140 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1905,6 +1905,38 @@ check_old_cluster_for_valid_slots(void)
 	check_ok();
 }
 
+/*
+ * Callback function for processing results of query for
+ * check_old_cluster_subscription_state()'s UpgradeTask.  If the query returned
+ * any rows (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_old_sub_state_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	int			ntup = PQntuples(res);
+	int			i_srsubstate = PQfnumber(res, "srsubstate");
+	int			i_subname = PQfnumber(res, "subname");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+
+	AssertVariableIsOfType(&process_old_sub_state_check, UpgradeTaskProcessCB);
+
+	for (int i = 0; i < ntup; i++)
+	{
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+
+		fprintf(report->file, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
+				PQgetvalue(res, i, i_srsubstate),
+				dbinfo->db_name,
+				PQgetvalue(res, i, i_subname),
+				PQgetvalue(res, i, i_nspname),
+				PQgetvalue(res, i, i_relname));
+	}
+}
+
 /*
  * check_old_cluster_subscription_state()
  *
@@ -1915,115 +1947,99 @@ check_old_cluster_for_valid_slots(void)
 static void
 check_old_cluster_subscription_state(void)
 {
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTask *task = upgrade_task_create();
+	UpgradeTaskReport report;
+	const char *query;
+	PGresult   *res;
+	PGconn	   *conn;
 	int			ntup;
 
 	prep_status("Checking for subscription state");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
 			 log_opts.basedir,
 			 "subs_invalid.txt");
-	for (int dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
-
-		/* We need to check for pg_replication_origin only once. */
-		if (dbnum == 0)
-		{
-			/*
-			 * Check that all the subscriptions have their respective
-			 * replication origin.
-			 */
-			res = executeQueryOrDie(conn,
-									"SELECT d.datname, s.subname "
-									"FROM pg_catalog.pg_subscription s "
-									"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
-									"	ON o.roname = 'pg_' || s.oid "
-									"INNER JOIN pg_catalog.pg_database d "
-									"	ON d.oid = s.subdbid "
-									"WHERE o.roname IS NULL;");
-
-			ntup = PQntuples(res);
-			for (int i = 0; i < ntup; i++)
-			{
-				if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-					pg_fatal("could not open file \"%s\": %m", output_path);
-				fprintf(script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
-						PQgetvalue(res, i, 0),
-						PQgetvalue(res, i, 1));
-			}
-			PQclear(res);
-		}
-
-		/*
-		 * We don't allow upgrade if there is a risk of dangling slot or
-		 * origin corresponding to initial sync after upgrade.
-		 *
-		 * A slot/origin not created yet refers to the 'i' (initialize) state,
-		 * while 'r' (ready) state refers to a slot/origin created previously
-		 * but already dropped. These states are supported for pg_upgrade. The
-		 * other states listed below are not supported:
-		 *
-		 * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state
-		 * would retain a replication slot, which could not be dropped by the
-		 * sync worker spawned after the upgrade because the subscription ID
-		 * used for the slot name won't match anymore.
-		 *
-		 * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state
-		 * would retain the replication origin when there is a failure in
-		 * tablesync worker immediately after dropping the replication slot in
-		 * the publisher.
-		 *
-		 * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on
-		 * a relation upgraded while in this state would expect an origin ID
-		 * with the OID of the subscription used before the upgrade, causing
-		 * it to fail.
-		 *
-		 * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and
-		 * SUBREL_STATE_UNKNOWN: These states are not stored in the catalog,
-		 * so we need not allow these states.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT r.srsubstate, s.subname, n.nspname, c.relname "
-								"FROM pg_catalog.pg_subscription_rel r "
-								"LEFT JOIN pg_catalog.pg_subscription s"
-								"	ON r.srsubid = s.oid "
-								"LEFT JOIN pg_catalog.pg_class c"
-								"	ON r.srrelid = c.oid "
-								"LEFT JOIN pg_catalog.pg_namespace n"
-								"	ON c.relnamespace = n.oid "
-								"WHERE r.srsubstate NOT IN ('i', 'r') "
-								"ORDER BY s.subname");
-
-		ntup = PQntuples(res);
-		for (int i = 0; i < ntup; i++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-
-			fprintf(script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
-					PQgetvalue(res, i, 0),
-					active_db->db_name,
-					PQgetvalue(res, i, 1),
-					PQgetvalue(res, i, 2),
-					PQgetvalue(res, i, 3));
-		}
 
-		PQclear(res);
-		PQfinish(conn);
+	/*
+	 * Check that all the subscriptions have their respective replication
+	 * origin.  This check only needs to run once.
+	 */
+	conn = connectToServer(&old_cluster, old_cluster.dbarr.dbs[0].db_name);
+	res = executeQueryOrDie(conn,
+							"SELECT d.datname, s.subname "
+							"FROM pg_catalog.pg_subscription s "
+							"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
+							"	ON o.roname = 'pg_' || s.oid "
+							"INNER JOIN pg_catalog.pg_database d "
+							"	ON d.oid = s.subdbid "
+							"WHERE o.roname IS NULL;");
+	ntup = PQntuples(res);
+	for (int i = 0; i < ntup; i++)
+	{
+		if (report.file == NULL &&
+			(report.file = fopen_priv(report.path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report.path);
+		fprintf(report.file, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
+				PQgetvalue(res, i, 0),
+				PQgetvalue(res, i, 1));
 	}
+	PQclear(res);
+	PQfinish(conn);
 
-	if (script)
+	/*
+	 * We don't allow upgrade if there is a risk of dangling slot or origin
+	 * corresponding to initial sync after upgrade.
+	 *
+	 * A slot/origin not created yet refers to the 'i' (initialize) state,
+	 * while 'r' (ready) state refers to a slot/origin created previously but
+	 * already dropped. These states are supported for pg_upgrade. The other
+	 * states listed below are not supported:
+	 *
+	 * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state would
+	 * retain a replication slot, which could not be dropped by the sync
+	 * worker spawned after the upgrade because the subscription ID used for
+	 * the slot name won't match anymore.
+	 *
+	 * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state would
+	 * retain the replication origin when there is a failure in tablesync
+	 * worker immediately after dropping the replication slot in the
+	 * publisher.
+	 *
+	 * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on a
+	 * relation upgraded while in this state would expect an origin ID with
+	 * the OID of the subscription used before the upgrade, causing it to
+	 * fail.
+	 *
+	 * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and
+	 * SUBREL_STATE_UNKNOWN: These states are not stored in the catalog, so we
+	 * need not allow these states.
+	 */
+	query = "SELECT r.srsubstate, s.subname, n.nspname, c.relname "
+		"FROM pg_catalog.pg_subscription_rel r "
+		"LEFT JOIN pg_catalog.pg_subscription s"
+		"   ON r.srsubid = s.oid "
+		"LEFT JOIN pg_catalog.pg_class c"
+		"   ON r.srrelid = c.oid "
+		"LEFT JOIN pg_catalog.pg_namespace n"
+		"   ON c.relnamespace = n.oid "
+		"WHERE r.srsubstate NOT IN ('i', 'r') "
+		"ORDER BY s.subname";
+
+	upgrade_task_add_step(task, query, process_old_sub_state_check,
+						  true, &report);
+
+	upgrade_task_run(task, &old_cluster);
+	upgrade_task_free(task);
+
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains subscriptions without origin or having relations not in i (initialize) or r (ready) state.\n"
 				 "You can allow the initial sync to finish for all relations and then restart the upgrade.\n"
 				 "A list of the problematic subscriptions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v10-0003-Use-pg_upgrade-s-new-parallel-framework-to-get-r.patchtext/plain; charset=us-asciiDownload
From 67479ba2693eec0f16208b3e43a2d22cba38a15e Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:09:33 -0500
Subject: [PATCH v10 03/11] Use pg_upgrade's new parallel framework to get
 relation info.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/info.c | 296 ++++++++++++++++++++------------------
 1 file changed, 154 insertions(+), 142 deletions(-)

diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index d3c1e8918d..2bfc8dcfba 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -11,6 +11,7 @@
 
 #include "access/transam.h"
 #include "catalog/pg_class_d.h"
+#include "pqexpbuffer.h"
 #include "pg_upgrade.h"
 
 static void create_rel_filename_map(const char *old_data, const char *new_data,
@@ -22,12 +23,14 @@ static void report_unmatched_relation(const RelInfo *rel, const DbInfo *db,
 static void free_db_and_rel_infos(DbInfoArr *db_arr);
 static void get_template0_info(ClusterInfo *cluster);
 static void get_db_infos(ClusterInfo *cluster);
-static void get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo);
+static char *get_rel_infos_query(void);
+static void process_rel_infos(DbInfo *dbinfo, PGresult *res, void *arg);
 static void free_rel_infos(RelInfoArr *rel_arr);
 static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
-static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo);
+static char *get_old_cluster_logical_slot_infos_query(void);
+static void process_old_cluster_logical_slot_infos(DbInfo *dbinfo, PGresult *res, void *arg);
 
 
 /*
@@ -276,7 +279,9 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
 void
 get_db_rel_and_slot_infos(ClusterInfo *cluster)
 {
-	int			dbnum;
+	UpgradeTask *task = upgrade_task_create();
+	char	   *rel_infos_query = NULL;
+	char	   *logical_slot_infos_query = NULL;
 
 	if (cluster->dbarr.dbs != NULL)
 		free_db_and_rel_infos(&cluster->dbarr);
@@ -284,15 +289,37 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster)
 	get_template0_info(cluster);
 	get_db_infos(cluster);
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	rel_infos_query = get_rel_infos_query();
+	upgrade_task_add_step(task,
+						  rel_infos_query,
+						  process_rel_infos,
+						  true, NULL);
+
+	/*
+	 * Logical slots are only carried over to the new cluster when the old
+	 * cluster is on PG17 or newer.  This is because before that the logical
+	 * slots are not saved at shutdown, so there is no guarantee that the
+	 * latest confirmed_flush_lsn is saved to disk which can lead to data
+	 * loss. It is still not guaranteed for manually created slots in PG17, so
+	 * subsequent checks done in check_old_cluster_for_valid_slots() would
+	 * raise a FATAL error if such slots are included.
+	 */
+	if (cluster == &old_cluster &&
+		GET_MAJOR_VERSION(cluster->major_version) > 1600)
 	{
-		DbInfo	   *pDbInfo = &cluster->dbarr.dbs[dbnum];
+		logical_slot_infos_query = get_old_cluster_logical_slot_infos_query();
+		upgrade_task_add_step(task,
+							  logical_slot_infos_query,
+							  process_old_cluster_logical_slot_infos,
+							  true, NULL);
+	}
 
-		get_rel_infos(cluster, pDbInfo);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
 
-		if (cluster == &old_cluster)
-			get_old_cluster_logical_slot_infos(pDbInfo);
-	}
+	pg_free(rel_infos_query);
+	if (logical_slot_infos_query)
+		pg_free(logical_slot_infos_query);
 
 	if (cluster == &old_cluster)
 		pg_log(PG_VERBOSE, "\nsource databases:");
@@ -431,40 +458,21 @@ get_db_infos(ClusterInfo *cluster)
 
 
 /*
- * get_rel_infos()
+ * get_rel_infos_query()
  *
- * gets the relinfos for all the user tables and indexes of the database
- * referred to by "dbinfo".
+ * Returns the query for retrieving the relation information for all the user
+ * tables and indexes in the database, for use by get_db_rel_and_slot_infos()'s
+ * UpgradeTask.
  *
- * Note: the resulting RelInfo array is assumed to be sorted by OID.
- * This allows later processing to match up old and new databases efficiently.
+ * Note: the result is assumed to be sorted by OID.  This allows later
+ * processing to match up old and new databases efficiently.
  */
-static void
-get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
+static char *
+get_rel_infos_query(void)
 {
-	PGconn	   *conn = connectToServer(cluster,
-									   dbinfo->db_name);
-	PGresult   *res;
-	RelInfo    *relinfos;
-	int			ntups;
-	int			relnum;
-	int			num_rels = 0;
-	char	   *nspname = NULL;
-	char	   *relname = NULL;
-	char	   *tablespace = NULL;
-	int			i_spclocation,
-				i_nspname,
-				i_relname,
-				i_reloid,
-				i_indtable,
-				i_toastheap,
-				i_relfilenumber,
-				i_reltablespace;
-	char		query[QUERY_ALLOC];
-	char	   *last_namespace = NULL,
-			   *last_tablespace = NULL;
+	PQExpBufferData query;
 
-	query[0] = '\0';			/* initialize query string to empty */
+	initPQExpBuffer(&query);
 
 	/*
 	 * Create a CTE that collects OIDs of regular user tables and matviews,
@@ -476,34 +484,34 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * output, so we have to copy that system table.  It's easiest to do that
 	 * by treating it as a user table.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "WITH regular_heap (reloid, indtable, toastheap) AS ( "
-			 "  SELECT c.oid, 0::oid, 0::oid "
-			 "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
-			 "         ON c.relnamespace = n.oid "
-			 "  WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
-			 CppAsString2(RELKIND_MATVIEW) ") AND "
+	appendPQExpBuffer(&query,
+					  "WITH regular_heap (reloid, indtable, toastheap) AS ( "
+					  "  SELECT c.oid, 0::oid, 0::oid "
+					  "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
+					  "         ON c.relnamespace = n.oid "
+					  "  WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
+					  CppAsString2(RELKIND_MATVIEW) ") AND "
 	/* exclude possible orphaned temp tables */
-			 "    ((n.nspname !~ '^pg_temp_' AND "
-			 "      n.nspname !~ '^pg_toast_temp_' AND "
-			 "      n.nspname NOT IN ('pg_catalog', 'information_schema', "
-			 "                        'binary_upgrade', 'pg_toast') AND "
-			 "      c.oid >= %u::pg_catalog.oid) OR "
-			 "     (n.nspname = 'pg_catalog' AND "
-			 "      relname IN ('pg_largeobject') ))), ",
-			 FirstNormalObjectId);
+					  "    ((n.nspname !~ '^pg_temp_' AND "
+					  "      n.nspname !~ '^pg_toast_temp_' AND "
+					  "      n.nspname NOT IN ('pg_catalog', 'information_schema', "
+					  "                        'binary_upgrade', 'pg_toast') AND "
+					  "      c.oid >= %u::pg_catalog.oid) OR "
+					  "     (n.nspname = 'pg_catalog' AND "
+					  "      relname IN ('pg_largeobject') ))), ",
+					  FirstNormalObjectId);
 
 	/*
 	 * Add a CTE that collects OIDs of toast tables belonging to the tables
 	 * selected by the regular_heap CTE.  (We have to do this separately
 	 * because the namespace-name rules above don't work for toast tables.)
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "  toast_heap (reloid, indtable, toastheap) AS ( "
-			 "  SELECT c.reltoastrelid, 0::oid, c.oid "
-			 "  FROM regular_heap JOIN pg_catalog.pg_class c "
-			 "      ON regular_heap.reloid = c.oid "
-			 "  WHERE c.reltoastrelid != 0), ");
+	appendPQExpBufferStr(&query,
+						 "  toast_heap (reloid, indtable, toastheap) AS ( "
+						 "  SELECT c.reltoastrelid, 0::oid, c.oid "
+						 "  FROM regular_heap JOIN pg_catalog.pg_class c "
+						 "      ON regular_heap.reloid = c.oid "
+						 "  WHERE c.reltoastrelid != 0), ");
 
 	/*
 	 * Add a CTE that collects OIDs of all valid indexes on the previously
@@ -511,53 +519,68 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * Testing indisready is necessary in 9.2, and harmless in earlier/later
 	 * versions.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "  all_index (reloid, indtable, toastheap) AS ( "
-			 "  SELECT indexrelid, indrelid, 0::oid "
-			 "  FROM pg_catalog.pg_index "
-			 "  WHERE indisvalid AND indisready "
-			 "    AND indrelid IN "
-			 "        (SELECT reloid FROM regular_heap "
-			 "         UNION ALL "
-			 "         SELECT reloid FROM toast_heap)) ");
+	appendPQExpBufferStr(&query,
+						 "  all_index (reloid, indtable, toastheap) AS ( "
+						 "  SELECT indexrelid, indrelid, 0::oid "
+						 "  FROM pg_catalog.pg_index "
+						 "  WHERE indisvalid AND indisready "
+						 "    AND indrelid IN "
+						 "        (SELECT reloid FROM regular_heap "
+						 "         UNION ALL "
+						 "         SELECT reloid FROM toast_heap)) ");
 
 	/*
 	 * And now we can write the query that retrieves the data we want for each
 	 * heap and index relation.  Make sure result is sorted by OID.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "SELECT all_rels.*, n.nspname, c.relname, "
-			 "  c.relfilenode, c.reltablespace, "
-			 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
-			 "FROM (SELECT * FROM regular_heap "
-			 "      UNION ALL "
-			 "      SELECT * FROM toast_heap "
-			 "      UNION ALL "
-			 "      SELECT * FROM all_index) all_rels "
-			 "  JOIN pg_catalog.pg_class c "
-			 "      ON all_rels.reloid = c.oid "
-			 "  JOIN pg_catalog.pg_namespace n "
-			 "     ON c.relnamespace = n.oid "
-			 "  LEFT OUTER JOIN pg_catalog.pg_tablespace t "
-			 "     ON c.reltablespace = t.oid "
-			 "ORDER BY 1;");
-
-	res = executeQueryOrDie(conn, "%s", query);
-
-	ntups = PQntuples(res);
+	appendPQExpBufferStr(&query,
+						 "SELECT all_rels.*, n.nspname, c.relname, "
+						 "  c.relfilenode, c.reltablespace, "
+						 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
+						 "FROM (SELECT * FROM regular_heap "
+						 "      UNION ALL "
+						 "      SELECT * FROM toast_heap "
+						 "      UNION ALL "
+						 "      SELECT * FROM all_index) all_rels "
+						 "  JOIN pg_catalog.pg_class c "
+						 "      ON all_rels.reloid = c.oid "
+						 "  JOIN pg_catalog.pg_namespace n "
+						 "     ON c.relnamespace = n.oid "
+						 "  LEFT OUTER JOIN pg_catalog.pg_tablespace t "
+						 "     ON c.reltablespace = t.oid "
+						 "ORDER BY 1;");
+
+	return query.data;
+}
 
-	relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+/*
+ * Callback function for processing results of the query returned by
+ * get_rel_infos_query(), which is used for get_db_rel_and_slot_infos()'s
+ * UpgradeTask.  This function stores the relation information for later use.
+ */
+static void
+process_rel_infos(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	int			ntups = PQntuples(res);
+	RelInfo    *relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	int			i_reloid = PQfnumber(res, "reloid");
+	int			i_indtable = PQfnumber(res, "indtable");
+	int			i_toastheap = PQfnumber(res, "toastheap");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+	int			i_relfilenumber = PQfnumber(res, "relfilenode");
+	int			i_reltablespace = PQfnumber(res, "reltablespace");
+	int			i_spclocation = PQfnumber(res, "spclocation");
+	int			num_rels = 0;
+	char	   *nspname = NULL;
+	char	   *relname = NULL;
+	char	   *tablespace = NULL;
+	char	   *last_namespace = NULL;
+	char	   *last_tablespace = NULL;
 
-	i_reloid = PQfnumber(res, "reloid");
-	i_indtable = PQfnumber(res, "indtable");
-	i_toastheap = PQfnumber(res, "toastheap");
-	i_nspname = PQfnumber(res, "nspname");
-	i_relname = PQfnumber(res, "relname");
-	i_relfilenumber = PQfnumber(res, "relfilenode");
-	i_reltablespace = PQfnumber(res, "reltablespace");
-	i_spclocation = PQfnumber(res, "spclocation");
+	AssertVariableIsOfType(&process_rel_infos, UpgradeTaskProcessCB);
 
-	for (relnum = 0; relnum < ntups; relnum++)
+	for (int relnum = 0; relnum < ntups; relnum++)
 	{
 		RelInfo    *curr = &relinfos[num_rels++];
 
@@ -610,44 +633,22 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 			/* A zero reltablespace oid indicates the database tablespace. */
 			curr->tablespace = dbinfo->db_tablespace;
 	}
-	PQclear(res);
-
-	PQfinish(conn);
 
 	dbinfo->rel_arr.rels = relinfos;
 	dbinfo->rel_arr.nrels = num_rels;
 }
 
 /*
- * get_old_cluster_logical_slot_infos()
- *
- * Gets the LogicalSlotInfos for all the logical replication slots of the
- * database referred to by "dbinfo". The status of each logical slot is gotten
- * here, but they are used at the checking phase. See
- * check_old_cluster_for_valid_slots().
+ * get_old_cluster_logical_slot_infos_query()
  *
- * Note: This function will not do anything if the old cluster is pre-PG17.
- * This is because before that the logical slots are not saved at shutdown, so
- * there is no guarantee that the latest confirmed_flush_lsn is saved to disk
- * which can lead to data loss. It is still not guaranteed for manually created
- * slots in PG17, so subsequent checks done in
- * check_old_cluster_for_valid_slots() would raise a FATAL error if such slots
- * are included.
+ * Returns the query for retrieving the logical slot information for all the
+ * logical replication slots in the database, for use by
+ * get_db_rel_and_slot_infos()'s UpgradeTask.  The status of each logical slot
+ * is checked in check_old_cluster_for_valid_slots().
  */
-static void
-get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
+static char *
+get_old_cluster_logical_slot_infos_query(void)
 {
-	PGconn	   *conn;
-	PGresult   *res;
-	LogicalSlotInfo *slotinfos = NULL;
-	int			num_slots;
-
-	/* Logical slots can be migrated since PG17. */
-	if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1600)
-		return;
-
-	conn = connectToServer(&old_cluster, dbinfo->db_name);
-
 	/*
 	 * Fetch the logical replication slot information. The check whether the
 	 * slot is considered caught up is done by an upgrade function. This
@@ -665,18 +666,32 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
-							"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
-							"FROM pg_catalog.pg_replication_slots "
-							"WHERE slot_type = 'logical' AND "
-							"database = current_database() AND "
-							"temporary IS FALSE;",
-							user_opts.live_check ? "FALSE" :
-							"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
-							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
-							"END)");
-
-	num_slots = PQntuples(res);
+	return psprintf("SELECT slot_name, plugin, two_phase, failover, "
+					"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
+					"FROM pg_catalog.pg_replication_slots "
+					"WHERE slot_type = 'logical' AND "
+					"database = current_database() AND "
+					"temporary IS FALSE;",
+					user_opts.live_check ? "FALSE" :
+					"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
+					"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
+					"END)");
+}
+
+/*
+ * Callback function for processing results of the query returned by
+ * get_old_cluster_logical_slot_infos_query(), which is used for
+ * get_db_rel_and_slot_infos()'s UpgradeTask.  This function stores the logical
+ * slot information for later use.
+ */
+static void
+process_old_cluster_logical_slot_infos(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	LogicalSlotInfo *slotinfos = NULL;
+	int			num_slots = PQntuples(res);
+
+	AssertVariableIsOfType(&process_old_cluster_logical_slot_infos,
+						   UpgradeTaskProcessCB);
 
 	if (num_slots)
 	{
@@ -709,9 +724,6 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 		}
 	}
 
-	PQclear(res);
-	PQfinish(conn);
-
 	dbinfo->slot_arr.slots = slotinfos;
 	dbinfo->slot_arr.nslots = num_slots;
 }
-- 
2.39.3 (Apple Git-146)

v10-0004-Use-pg_upgrade-s-new-parallel-framework-to-get-l.patchtext/plain; charset=us-asciiDownload
From 0abadb72bf3675b0d48721b69bdaa5a734827112 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 13:32:33 -0500
Subject: [PATCH v10 04/11] Use pg_upgrade's new parallel framework to get
 loadable libraries.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/function.c | 71 ++++++++++++++++++++++-------------
 1 file changed, 45 insertions(+), 26 deletions(-)

diff --git a/src/bin/pg_upgrade/function.c b/src/bin/pg_upgrade/function.c
index 7e3abed098..0588347b49 100644
--- a/src/bin/pg_upgrade/function.c
+++ b/src/bin/pg_upgrade/function.c
@@ -42,6 +42,30 @@ library_name_compare(const void *p1, const void *p2)
 					  ((const LibraryInfo *) p2)->dbnum);
 }
 
+/*
+ * Private state for get_loadable_libraries()'s UpgradeTask.
+ */
+struct loadable_libraries_state
+{
+	PGresult  **ress;			/* results for each database */
+	int			totaltups;		/* number of tuples in all results */
+};
+
+/*
+ * Callback function for processing results of query for
+ * get_loadable_libraries()'s UpgradeTask.  This function stores the results
+ * for later use within get_loadable_libraries().
+ */
+static void
+process_loadable_libraries(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct loadable_libraries_state *state = (struct loadable_libraries_state *) arg;
+
+	AssertVariableIsOfType(&process_loadable_libraries, UpgradeTaskProcessCB);
+
+	state->ress[dbinfo - old_cluster.dbarr.dbs] = res;
+	state->totaltups += PQntuples(res);
+}
 
 /*
  * get_loadable_libraries()
@@ -54,47 +78,41 @@ library_name_compare(const void *p1, const void *p2)
 void
 get_loadable_libraries(void)
 {
-	PGresult  **ress;
 	int			totaltups;
 	int			dbnum;
 	int			n_libinfos;
+	UpgradeTask *task = upgrade_task_create();
+	struct loadable_libraries_state state;
+	char	   *query;
 
-	ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
-	totaltups = 0;
+	state.ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
+	state.totaltups = 0;
 
-	/* Fetch all library names, removing duplicates within each DB */
-	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
+	query = psprintf("SELECT DISTINCT probin "
+					 "FROM pg_catalog.pg_proc "
+					 "WHERE prolang = %u AND "
+					 "probin IS NOT NULL AND "
+					 "oid >= %u;",
+					 ClanguageId,
+					 FirstNormalObjectId);
 
-		/*
-		 * Fetch all libraries containing non-built-in C functions in this DB.
-		 */
-		ress[dbnum] = executeQueryOrDie(conn,
-										"SELECT DISTINCT probin "
-										"FROM pg_catalog.pg_proc "
-										"WHERE prolang = %u AND "
-										"probin IS NOT NULL AND "
-										"oid >= %u;",
-										ClanguageId,
-										FirstNormalObjectId);
-		totaltups += PQntuples(ress[dbnum]);
-
-		PQfinish(conn);
-	}
+	upgrade_task_add_step(task, query, process_loadable_libraries,
+						  false, &state);
+
+	upgrade_task_run(task, &old_cluster);
+	upgrade_task_free(task);
 
 	/*
 	 * Allocate memory for required libraries and logical replication output
 	 * plugins.
 	 */
-	n_libinfos = totaltups + count_old_cluster_logical_slots();
+	n_libinfos = state.totaltups + count_old_cluster_logical_slots();
 	os_info.libraries = (LibraryInfo *) pg_malloc(sizeof(LibraryInfo) * n_libinfos);
 	totaltups = 0;
 
 	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
 	{
-		PGresult   *res = ress[dbnum];
+		PGresult   *res = state.ress[dbnum];
 		int			ntups;
 		int			rowno;
 		LogicalSlotInfoArr *slot_arr = &old_cluster.dbarr.dbs[dbnum].slot_arr;
@@ -129,7 +147,8 @@ get_loadable_libraries(void)
 		}
 	}
 
-	pg_free(ress);
+	pg_free(state.ress);
+	pg_free(query);
 
 	os_info.num_libraries = totaltups;
 }
-- 
2.39.3 (Apple Git-146)

v10-0005-Use-pg_upgrade-s-new-parallel-framework-for-exte.patchtext/plain; charset=us-asciiDownload
From d12f81e0320b388434b24d1d679571764e7fbd06 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 14:18:39 -0500
Subject: [PATCH v10 05/11] Use pg_upgrade's new parallel framework for
 extension updates.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/version.c | 94 +++++++++++++++++++-----------------
 1 file changed, 49 insertions(+), 45 deletions(-)

diff --git a/src/bin/pg_upgrade/version.c b/src/bin/pg_upgrade/version.c
index 2de6dffccd..5084b08805 100644
--- a/src/bin/pg_upgrade/version.c
+++ b/src/bin/pg_upgrade/version.c
@@ -139,6 +139,41 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 		check_ok();
 }
 
+/*
+ * Callback function for processing results of query for
+ * report_extension_updates()'s UpgradeTask.  If the query returned any rows,
+ * write the details to the report file.
+ */
+static void
+process_extension_updates(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_name = PQfnumber(res, "name");
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+
+	AssertVariableIsOfType(&process_extension_updates, UpgradeTaskProcessCB);
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
+		{
+			PQExpBufferData connectbuf;
+
+			initPQExpBuffer(&connectbuf);
+			appendPsqlMetaConnect(&connectbuf, dbinfo->db_name);
+			fputs(connectbuf.data, report->file);
+			termPQExpBuffer(&connectbuf);
+			db_used = true;
+		}
+		fprintf(report->file, "ALTER EXTENSION %s UPDATE;\n",
+				quote_identifier(PQgetvalue(res, rowno, i_name)));
+	}
+}
+
 /*
  * report_extension_updates()
  *	Report extensions that should be updated.
@@ -146,57 +181,26 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 void
 report_extension_updates(ClusterInfo *cluster)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char	   *output_path = "update_extensions.sql";
+	UpgradeTaskReport report;
+	UpgradeTask *task = upgrade_task_create();
+	const char *query = "SELECT name "
+		"FROM pg_available_extensions "
+		"WHERE installed_version != default_version";
 
 	prep_status("Checking for extension updates");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_name;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* find extensions needing updates */
-		res = executeQueryOrDie(conn,
-								"SELECT name "
-								"FROM pg_available_extensions "
-								"WHERE installed_version != default_version"
-			);
+	report.file = NULL;
+	strcpy(report.path, "update_extensions.sql");
 
-		ntups = PQntuples(res);
-		i_name = PQfnumber(res, "name");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				PQExpBufferData connectbuf;
+	upgrade_task_add_step(task, query, process_extension_updates,
+						  true, &report);
 
-				initPQExpBuffer(&connectbuf);
-				appendPsqlMetaConnect(&connectbuf, active_db->db_name);
-				fputs(connectbuf.data, script);
-				termPQExpBuffer(&connectbuf);
-				db_used = true;
-			}
-			fprintf(script, "ALTER EXTENSION %s UPDATE;\n",
-					quote_identifier(PQgetvalue(res, rowno, i_name)));
-		}
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
 
-		PQclear(res);
-
-		PQfinish(conn);
-	}
-
-	if (script)
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		report_status(PG_REPORT, "notice");
 		pg_log(PG_REPORT, "\n"
 			   "Your installation contains extensions that should be updated\n"
@@ -204,7 +208,7 @@ report_extension_updates(ClusterInfo *cluster)
 			   "    %s\n"
 			   "when executed by psql by the database superuser will update\n"
 			   "these extensions.",
-			   output_path);
+			   report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v10-0006-Use-pg_upgrade-s-new-parallel-framework-for-data.patchtext/plain; charset=us-asciiDownload
From 7a5405cdd97bfb0a4a69167c412ef47b4380ba94 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Sat, 6 Jul 2024 21:06:31 -0500
Subject: [PATCH v10 06/11] Use pg_upgrade's new parallel framework for data
 type checks.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 351 ++++++++++++++++++++-----------------
 1 file changed, 191 insertions(+), 160 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index f8160e0140..f935b53e1f 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -314,6 +314,147 @@ static DataTypesUsageChecks data_types_usage_checks[] =
 	}
 };
 
+/*
+ * Private state for check_for_data_types_usage()'s UpgradeTask.
+ */
+struct data_type_check_state
+{
+	DataTypesUsageChecks *check;	/* the check for this step */
+	bool	   *result;			/* true if check failed for any database */
+	PQExpBuffer *report;		/* buffer for report on failed checks */
+};
+
+/*
+ * Returns a palloc'd query string for the data type check, for use by
+ * check_for_data_types_usage()'s UpgradeTask.
+ */
+static char *
+data_type_check_query(int checknum)
+{
+	DataTypesUsageChecks *check = &data_types_usage_checks[checknum];
+
+	return psprintf("WITH RECURSIVE oids AS ( "
+	/* start with the type(s) returned by base_query */
+					"	%s "
+					"	UNION ALL "
+					"	SELECT * FROM ( "
+	/* inner WITH because we can only reference the CTE once */
+					"		WITH x AS (SELECT oid FROM oids) "
+	/* domains on any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
+					"			UNION ALL "
+	/* arrays over any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
+					"			UNION ALL "
+	/* composite types containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
+					"			WHERE t.typtype = 'c' AND "
+					"				  t.oid = c.reltype AND "
+					"				  c.oid = a.attrelid AND "
+					"				  NOT a.attisdropped AND "
+					"				  a.atttypid = x.oid "
+					"			UNION ALL "
+	/* ranges containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
+					"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
+					"	) foo "
+					") "
+	/* now look for stored columns of any such type */
+					"SELECT n.nspname, c.relname, a.attname "
+					"FROM	pg_catalog.pg_class c, "
+					"		pg_catalog.pg_namespace n, "
+					"		pg_catalog.pg_attribute a "
+					"WHERE	c.oid = a.attrelid AND "
+					"		NOT a.attisdropped AND "
+					"		a.atttypid IN (SELECT oid FROM oids) AND "
+					"		c.relkind IN ("
+					CppAsString2(RELKIND_RELATION) ", "
+					CppAsString2(RELKIND_MATVIEW) ", "
+					CppAsString2(RELKIND_INDEX) ") AND "
+					"		c.relnamespace = n.oid AND "
+	/* exclude possible orphaned temp tables */
+					"		n.nspname !~ '^pg_temp_' AND "
+					"		n.nspname !~ '^pg_toast_temp_' AND "
+	/* exclude system catalogs, too */
+					"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
+					check->base_query);
+}
+
+/*
+ * Callback function for processing results of queries for
+ * check_for_data_types_usage()'s UpgradeTask.  If the query returned any rows
+ * (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_data_type_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct data_type_check_state *state = (struct data_type_check_state *) arg;
+	int			ntups = PQntuples(res);
+
+	AssertVariableIsOfType(&process_data_type_check, UpgradeTaskProcessCB);
+
+	if (ntups)
+	{
+		char		output_path[MAXPGPATH];
+		int			i_nspname;
+		int			i_relname;
+		int			i_attname;
+		FILE	   *script = NULL;
+		bool		db_used = false;
+
+		snprintf(output_path, sizeof(output_path), "%s/%s",
+				 log_opts.basedir,
+				 state->check->report_filename);
+
+		/*
+		 * Make sure we have a buffer to save reports to now that we found a
+		 * first failing check.
+		 */
+		if (*state->report == NULL)
+			*state->report = createPQExpBuffer();
+
+		/*
+		 * If this is the first time we see an error for the check in question
+		 * then print a status message of the failure.
+		 */
+		if (!(*state->result))
+		{
+			pg_log(PG_REPORT, "    failed check: %s", _(state->check->status));
+			appendPQExpBuffer(*state->report, "\n%s\n%s    %s\n",
+							  _(state->check->report_text),
+							  _("A list of the problem columns is in the file:"),
+							  output_path);
+		}
+		*state->result = true;
+
+		i_nspname = PQfnumber(res, "nspname");
+		i_relname = PQfnumber(res, "relname");
+		i_attname = PQfnumber(res, "attname");
+
+		for (int rowno = 0; rowno < ntups; rowno++)
+		{
+			if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
+				pg_fatal("could not open file \"%s\": %m", output_path);
+
+			if (!db_used)
+			{
+				fprintf(script, "In database: %s\n", dbinfo->db_name);
+				db_used = true;
+			}
+			fprintf(script, "  %s.%s.%s\n",
+					PQgetvalue(res, rowno, i_nspname),
+					PQgetvalue(res, rowno, i_relname),
+					PQgetvalue(res, rowno, i_attname));
+		}
+
+		if (script)
+		{
+			fclose(script);
+			script = NULL;
+		}
+	}
+}
+
 /*
  * check_for_data_types_usage()
  *	Detect whether there are any stored columns depending on given type(s)
@@ -334,13 +475,15 @@ static DataTypesUsageChecks data_types_usage_checks[] =
  * there's no storage involved in a view.
  */
 static void
-check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
+check_for_data_types_usage(ClusterInfo *cluster)
 {
-	bool		found = false;
 	bool	   *results;
-	PQExpBufferData report;
-	DataTypesUsageChecks *tmp = checks;
+	PQExpBuffer report = NULL;
+	DataTypesUsageChecks *tmp = data_types_usage_checks;
 	int			n_data_types_usage_checks = 0;
+	UpgradeTask *task = upgrade_task_create();
+	char	  **queries = NULL;
+	struct data_type_check_state *states;
 
 	prep_status("Checking data type usage");
 
@@ -353,175 +496,63 @@ check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
 
 	/* Prepare an array to store the results of checks in */
 	results = pg_malloc0(sizeof(bool) * n_data_types_usage_checks);
+	queries = pg_malloc0(sizeof(char *) * n_data_types_usage_checks);
+	states = pg_malloc0(sizeof(struct data_type_check_state) * n_data_types_usage_checks);
 
-	/*
-	 * Connect to each database in the cluster and run all defined checks
-	 * against that database before trying the next one.
-	 */
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int i = 0; i < n_data_types_usage_checks; i++)
 	{
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
+		DataTypesUsageChecks *check = &data_types_usage_checks[i];
 
-		for (int checknum = 0; checknum < n_data_types_usage_checks; checknum++)
+		if (check->threshold_version == MANUAL_CHECK)
 		{
-			PGresult   *res;
-			int			ntups;
-			int			i_nspname;
-			int			i_relname;
-			int			i_attname;
-			FILE	   *script = NULL;
-			bool		db_used = false;
-			char		output_path[MAXPGPATH];
-			DataTypesUsageChecks *cur_check = &checks[checknum];
-
-			if (cur_check->threshold_version == MANUAL_CHECK)
-			{
-				Assert(cur_check->version_hook);
-
-				/*
-				 * Make sure that the check applies to the current cluster
-				 * version and skip if not. If no check hook has been defined
-				 * we run the check for all versions.
-				 */
-				if (!cur_check->version_hook(cluster))
-					continue;
-			}
-			else if (cur_check->threshold_version != ALL_VERSIONS)
-			{
-				if (GET_MAJOR_VERSION(cluster->major_version) > cur_check->threshold_version)
-					continue;
-			}
-			else
-				Assert(cur_check->threshold_version == ALL_VERSIONS);
-
-			snprintf(output_path, sizeof(output_path), "%s/%s",
-					 log_opts.basedir,
-					 cur_check->report_filename);
+			Assert(check->version_hook);
 
 			/*
-			 * The type(s) of interest might be wrapped in a domain, array,
-			 * composite, or range, and these container types can be nested
-			 * (to varying extents depending on server version, but that's not
-			 * of concern here).  To handle all these cases we need a
-			 * recursive CTE.
+			 * Make sure that the check applies to the current cluster version
+			 * and skip it if not.
 			 */
-			res = executeQueryOrDie(conn,
-									"WITH RECURSIVE oids AS ( "
-			/* start with the type(s) returned by base_query */
-									"	%s "
-									"	UNION ALL "
-									"	SELECT * FROM ( "
-			/* inner WITH because we can only reference the CTE once */
-									"		WITH x AS (SELECT oid FROM oids) "
-			/* domains on any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
-									"			UNION ALL "
-			/* arrays over any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
-									"			UNION ALL "
-			/* composite types containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
-									"			WHERE t.typtype = 'c' AND "
-									"				  t.oid = c.reltype AND "
-									"				  c.oid = a.attrelid AND "
-									"				  NOT a.attisdropped AND "
-									"				  a.atttypid = x.oid "
-									"			UNION ALL "
-			/* ranges containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
-									"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
-									"	) foo "
-									") "
-			/* now look for stored columns of any such type */
-									"SELECT n.nspname, c.relname, a.attname "
-									"FROM	pg_catalog.pg_class c, "
-									"		pg_catalog.pg_namespace n, "
-									"		pg_catalog.pg_attribute a "
-									"WHERE	c.oid = a.attrelid AND "
-									"		NOT a.attisdropped AND "
-									"		a.atttypid IN (SELECT oid FROM oids) AND "
-									"		c.relkind IN ("
-									CppAsString2(RELKIND_RELATION) ", "
-									CppAsString2(RELKIND_MATVIEW) ", "
-									CppAsString2(RELKIND_INDEX) ") AND "
-									"		c.relnamespace = n.oid AND "
-			/* exclude possible orphaned temp tables */
-									"		n.nspname !~ '^pg_temp_' AND "
-									"		n.nspname !~ '^pg_toast_temp_' AND "
-			/* exclude system catalogs, too */
-									"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
-									cur_check->base_query);
-
-			ntups = PQntuples(res);
+			if (!check->version_hook(cluster))
+				continue;
+		}
+		else if (check->threshold_version != ALL_VERSIONS)
+		{
+			if (GET_MAJOR_VERSION(cluster->major_version) > check->threshold_version)
+				continue;
+		}
+		else
+			Assert(check->threshold_version == ALL_VERSIONS);
 
-			/*
-			 * The datatype was found, so extract the data and log to the
-			 * requested filename. We need to open the file for appending
-			 * since the check might have already found the type in another
-			 * database earlier in the loop.
-			 */
-			if (ntups)
-			{
-				/*
-				 * Make sure we have a buffer to save reports to now that we
-				 * found a first failing check.
-				 */
-				if (!found)
-					initPQExpBuffer(&report);
-				found = true;
-
-				/*
-				 * If this is the first time we see an error for the check in
-				 * question then print a status message of the failure.
-				 */
-				if (!results[checknum])
-				{
-					pg_log(PG_REPORT, "    failed check: %s", _(cur_check->status));
-					appendPQExpBuffer(&report, "\n%s\n%s    %s\n",
-									  _(cur_check->report_text),
-									  _("A list of the problem columns is in the file:"),
-									  output_path);
-				}
-				results[checknum] = true;
-
-				i_nspname = PQfnumber(res, "nspname");
-				i_relname = PQfnumber(res, "relname");
-				i_attname = PQfnumber(res, "attname");
-
-				for (int rowno = 0; rowno < ntups; rowno++)
-				{
-					if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
-						pg_fatal("could not open file \"%s\": %m", output_path);
-
-					if (!db_used)
-					{
-						fprintf(script, "In database: %s\n", active_db->db_name);
-						db_used = true;
-					}
-					fprintf(script, "  %s.%s.%s\n",
-							PQgetvalue(res, rowno, i_nspname),
-							PQgetvalue(res, rowno, i_relname),
-							PQgetvalue(res, rowno, i_attname));
-				}
-
-				if (script)
-				{
-					fclose(script);
-					script = NULL;
-				}
-			}
+		queries[i] = data_type_check_query(i);
 
-			PQclear(res);
-		}
+		states[i].check = check;
+		states[i].result = &results[i];
+		states[i].report = &report;
 
-		PQfinish(conn);
+		upgrade_task_add_step(task, queries[i], process_data_type_check,
+							  true, &states[i]);
 	}
 
-	if (found)
-		pg_fatal("Data type checks failed: %s", report.data);
+	/*
+	 * Connect to each database in the cluster and run all defined checks
+	 * against that database before trying the next one.
+	 */
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
+
+	if (report)
+	{
+		pg_fatal("Data type checks failed: %s", report->data);
+		destroyPQExpBuffer(report);
+	}
 
 	pg_free(results);
+	for (int i = 0; i < n_data_types_usage_checks; i++)
+	{
+		if (queries[i])
+			pg_free(queries[i]);
+	}
+	pg_free(queries);
+	pg_free(states);
 
 	check_ok();
 }
@@ -616,7 +647,7 @@ check_and_dump_old_cluster(void)
 		check_old_cluster_subscription_state();
 	}
 
-	check_for_data_types_usage(&old_cluster, data_types_usage_checks);
+	check_for_data_types_usage(&old_cluster);
 
 	/*
 	 * PG 14 changed the function signature of encoding conversion functions.
-- 
2.39.3 (Apple Git-146)

v10-0007-Use-pg_upgrade-s-new-parallel-framework-for-isn-.patchtext/plain; charset=us-asciiDownload
From dd239126aa373da9ab86f5cd8b9ec90d4e8fb64a Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:00:20 -0500
Subject: [PATCH v10 07/11] Use pg_upgrade's new parallel framework for isn and
 int8 check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 97 ++++++++++++++++++++------------------
 1 file changed, 50 insertions(+), 47 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index f935b53e1f..b8af7e541b 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1225,6 +1225,39 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 	check_ok();
 }
 
+/*
+ * Callback function for processing result of query for
+ * check_for_isn_and_int8_passing_mismatch()'s UpgradeTask.  If the query
+ * returned any rows (i.e., the check failed), write the details to the report
+ * file.
+ */
+static void
+process_isn_and_int8_passing_mismatch(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_proname = PQfnumber(res, "proname");
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+
+	AssertVariableIsOfType(&process_isn_and_int8_passing_mismatch,
+						   UpgradeTaskProcessCB);
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
+		{
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(report->file, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_proname));
+	}
+}
 
 /*
  *	check_for_isn_and_int8_passing_mismatch()
@@ -1236,9 +1269,13 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 static void
 check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTask *task;
+	UpgradeTaskReport report;
+	const char *query = "SELECT n.nspname, p.proname "
+		"FROM   pg_catalog.pg_proc p, "
+		"       pg_catalog.pg_namespace n "
+		"WHERE  p.pronamespace = n.oid AND "
+		"       p.probin = '$libdir/isn'";
 
 	prep_status("Checking for contrib/isn with bigint-passing mismatch");
 
@@ -1250,54 +1287,20 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 		return;
 	}
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
 			 log_opts.basedir,
 			 "contrib_isn_and_int8_pass_by_value.txt");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_proname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* Find any functions coming from contrib/isn */
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, p.proname "
-								"FROM	pg_catalog.pg_proc p, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	p.pronamespace = n.oid AND "
-								"		p.probin = '$libdir/isn'");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_proname = PQfnumber(res, "proname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_proname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	task = upgrade_task_create();
+	upgrade_task_add_step(task, query, process_isn_and_int8_passing_mismatch,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
 
-	if (script)
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains \"contrib/isn\" functions which rely on the\n"
 				 "bigint data type.  Your old and new clusters pass bigint values\n"
@@ -1305,7 +1308,7 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 				 "manually dump databases in the old cluster that use \"contrib/isn\"\n"
 				 "facilities, drop them, perform the upgrade, and then restore them.  A\n"
 				 "list of the problem functions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v10-0008-Use-pg_upgrade-s-new-parallel-framework-for-post.patchtext/plain; charset=us-asciiDownload
From ac3190b807bdac244d448f9abc7cc79782349d26 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 15:10:19 -0500
Subject: [PATCH v10 08/11] Use pg_upgrade's new parallel framework for postfix
 operator check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 146 +++++++++++++++++++------------------
 1 file changed, 75 insertions(+), 71 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index b8af7e541b..28c4ddbca3 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1315,95 +1315,99 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 }
 
 /*
- * Verify that no user defined postfix operators exist.
+ * Callback function for processing result of query for
+ * check_for_user_defined_postfix_ops()'s UpgradeTask.  If the query returned
+ * any rows (i.e., the check failed), write the details to the report file.
  */
 static void
-check_for_user_defined_postfix_ops(ClusterInfo *cluster)
+process_user_defined_postfix_ops(DbInfo *dbinfo, PGresult *res, void *arg)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	int			ntups = PQntuples(res);
+	bool		db_used = false;
+	int			i_oproid = PQfnumber(res, "oproid");
+	int			i_oprnsp = PQfnumber(res, "oprnsp");
+	int			i_oprname = PQfnumber(res, "oprname");
+	int			i_typnsp = PQfnumber(res, "typnsp");
+	int			i_typname = PQfnumber(res, "typname");
 
-	prep_status("Checking for user-defined postfix operators");
+	AssertVariableIsOfType(&process_user_defined_postfix_ops,
+						   UpgradeTaskProcessCB);
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
-			 log_opts.basedir,
-			 "postfix_ops.txt");
+	if (!ntups)
+		return;
 
-	/* Find any user defined postfix operators */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int rowno = 0; rowno < ntups; rowno++)
 	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_oproid,
-					i_oprnsp,
-					i_oprname,
-					i_typnsp,
-					i_typname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT o.oid AS oproid, "
-								"       n.nspname AS oprnsp, "
-								"       o.oprname, "
-								"       tn.nspname AS typnsp, "
-								"       t.typname "
-								"FROM pg_catalog.pg_operator o, "
-								"     pg_catalog.pg_namespace n, "
-								"     pg_catalog.pg_type t, "
-								"     pg_catalog.pg_namespace tn "
-								"WHERE o.oprnamespace = n.oid AND "
-								"      o.oprleft = t.oid AND "
-								"      t.typnamespace = tn.oid AND "
-								"      o.oprright = 0 AND "
-								"      o.oid >= 16384");
-		ntups = PQntuples(res);
-		i_oproid = PQfnumber(res, "oproid");
-		i_oprnsp = PQfnumber(res, "oprnsp");
-		i_oprname = PQfnumber(res, "oprname");
-		i_typnsp = PQfnumber(res, "typnsp");
-		i_typname = PQfnumber(res, "typname");
-		for (rowno = 0; rowno < ntups; rowno++)
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
 		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
-					PQgetvalue(res, rowno, i_oproid),
-					PQgetvalue(res, rowno, i_oprnsp),
-					PQgetvalue(res, rowno, i_oprname),
-					PQgetvalue(res, rowno, i_typnsp),
-					PQgetvalue(res, rowno, i_typname));
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
 		}
+		fprintf(report->file, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
+				PQgetvalue(res, rowno, i_oproid),
+				PQgetvalue(res, rowno, i_oprnsp),
+				PQgetvalue(res, rowno, i_oprname),
+				PQgetvalue(res, rowno, i_typnsp),
+				PQgetvalue(res, rowno, i_typname));
+	}
+}
 
-		PQclear(res);
+/*
+ * Verify that no user defined postfix operators exist.
+ */
+static void
+check_for_user_defined_postfix_ops(ClusterInfo *cluster)
+{
+	UpgradeTaskReport report;
+	UpgradeTask *task = upgrade_task_create();
+	const char *query;
 
-		PQfinish(conn);
-	}
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	query = "SELECT o.oid AS oproid, "
+		"       n.nspname AS oprnsp, "
+		"       o.oprname, "
+		"       tn.nspname AS typnsp, "
+		"       t.typname "
+		"FROM pg_catalog.pg_operator o, "
+		"     pg_catalog.pg_namespace n, "
+		"     pg_catalog.pg_type t, "
+		"     pg_catalog.pg_namespace tn "
+		"WHERE o.oprnamespace = n.oid AND "
+		"      o.oprleft = t.oid AND "
+		"      t.typnamespace = tn.oid AND "
+		"      o.oprright = 0 AND "
+		"      o.oid >= 16384";
 
-	if (script)
+	prep_status("Checking for user-defined postfix operators");
+
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
+			 log_opts.basedir,
+			 "postfix_ops.txt");
+
+	upgrade_task_add_step(task, query, process_user_defined_postfix_ops,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
+
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains user-defined postfix operators, which are not\n"
 				 "supported anymore.  Consider dropping the postfix operators and replacing\n"
 				 "them with prefix operators or function calls.\n"
 				 "A list of user-defined postfix operators is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v10-0009-Use-pg_upgrade-s-new-parallel-framework-for-poly.patchtext/plain; charset=us-asciiDownload
From d756f173cf94128c6a4fe11b9a48b90d33631298 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 15:21:29 -0500
Subject: [PATCH v10 09/11] Use pg_upgrade's new parallel framework for
 polymorphics check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 159 +++++++++++++++++++------------------
 1 file changed, 83 insertions(+), 76 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 28c4ddbca3..92a3aa6a77 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1413,6 +1413,40 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 		check_ok();
 }
 
+/*
+ * Callback function for processing results of query for
+ * check_for_incompatible_polymorphics()'s UpgradeTask.  If the query returned
+ * any rows (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_incompat_polymorphics(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_objkind = PQfnumber(res, "objkind");
+	int			i_objname = PQfnumber(res, "objname");
+
+	AssertVariableIsOfType(&process_incompat_polymorphics,
+						   UpgradeTaskProcessCB);
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
+		{
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+
+		fprintf(report->file, "  %s: %s\n",
+				PQgetvalue(res, rowno, i_objkind),
+				PQgetvalue(res, rowno, i_objname));
+	}
+}
+
 /*
  *	check_for_incompatible_polymorphics()
  *
@@ -1422,14 +1456,15 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 static void
 check_for_incompatible_polymorphics(ClusterInfo *cluster)
 {
-	PGresult   *res;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
 	PQExpBufferData old_polymorphics;
+	UpgradeTask *task = upgrade_task_create();
+	UpgradeTaskReport report;
+	char	   *query;
 
 	prep_status("Checking for incompatible polymorphic functions");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
 			 log_opts.basedir,
 			 "incompatible_polymorphics.txt");
 
@@ -1453,80 +1488,51 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 							 ", 'array_positions(anyarray,anyelement)'"
 							 ", 'width_bucket(anyelement,anyarray)'");
 
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		bool		db_used = false;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-		int			ntups;
-		int			i_objkind,
-					i_objname;
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-		/* Aggregate transition functions */
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Aggregate final functions */
-								"UNION ALL "
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Operators */
-								"UNION ALL "
-								"SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
-								"FROM pg_operator AS op "
-								"WHERE op.oid >= 16384 "
-								"AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
-								old_polymorphics.data,
-								old_polymorphics.data,
-								old_polymorphics.data);
-
-		ntups = PQntuples(res);
-
-		i_objkind = PQfnumber(res, "objkind");
-		i_objname = PQfnumber(res, "objname");
-
-		for (int rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-
-			fprintf(script, "  %s: %s\n",
-					PQgetvalue(res, rowno, i_objkind),
-					PQgetvalue(res, rowno, i_objname));
-		}
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
 
-		PQclear(res);
-		PQfinish(conn);
-	}
+	/* Aggregate transition functions */
+	query = psprintf("SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					 "FROM pg_proc AS p "
+					 "JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					 "JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
+					 "WHERE p.oid >= 16384 "
+					 "AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Aggregate final functions */
+					 "UNION ALL "
+					 "SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					 "FROM pg_proc AS p "
+					 "JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					 "JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
+					 "WHERE p.oid >= 16384 "
+					 "AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Operators */
+					 "UNION ALL "
+					 "SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
+					 "FROM pg_operator AS op "
+					 "WHERE op.oid >= 16384 "
+					 "AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
+					 old_polymorphics.data,
+					 old_polymorphics.data,
+					 old_polymorphics.data);
+
+	upgrade_task_add_step(task, query, process_incompat_polymorphics,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
 
-	if (script)
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains user-defined objects that refer to internal\n"
 				 "polymorphic functions with arguments of type \"anyarray\" or \"anyelement\".\n"
@@ -1534,12 +1540,13 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 				 "afterwards, changing them to refer to the new corresponding functions with\n"
 				 "arguments of type \"anycompatiblearray\" and \"anycompatible\".\n"
 				 "A list of the problematic objects is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
 
 	termPQExpBuffer(&old_polymorphics);
+	pg_free(query);
 }
 
 /*
-- 
2.39.3 (Apple Git-146)

v10-0010-Use-pg_upgrade-s-new-parallel-framework-for-WITH.patchtext/plain; charset=us-asciiDownload
From a6a7710e22d8d879a389de5777f724b45ed2e6f2 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 15:27:37 -0500
Subject: [PATCH v10 10/11] Use pg_upgrade's new parallel framework for WITH
 OIDS check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 100 +++++++++++++++++++------------------
 1 file changed, 52 insertions(+), 48 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 92a3aa6a77..dff440b29a 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1550,72 +1550,76 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 }
 
 /*
- * Verify that no tables are declared WITH OIDS.
+ * Callback function for processing results of query for
+ * check_for_tables_with_oids()'s UpgradeTask.  If the query returned any rows
+ * (i.e., the check failed), write the details to the report file.
  */
 static void
-check_for_tables_with_oids(ClusterInfo *cluster)
+process_with_oids_check(DbInfo *dbinfo, PGresult *res, void *arg)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
 
-	prep_status("Checking for tables WITH OIDS");
+	AssertVariableIsOfType(&process_with_oids_check, UpgradeTaskProcessCB);
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
-			 log_opts.basedir,
-			 "tables_with_oids.txt");
+	if (!ntups)
+		return;
 
-	/* Find any tables declared WITH OIDS */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int rowno = 0; rowno < ntups; rowno++)
 	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_relname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, c.relname "
-								"FROM	pg_catalog.pg_class c, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	c.relnamespace = n.oid AND "
-								"		c.relhasoids AND"
-								"       n.nspname NOT IN ('pg_catalog')");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_relname = PQfnumber(res, "relname");
-		for (rowno = 0; rowno < ntups; rowno++)
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
 		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_relname));
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
 		}
+		fprintf(report->file, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_relname));
+	}
+}
 
-		PQclear(res);
+/*
+ * Verify that no tables are declared WITH OIDS.
+ */
+static void
+check_for_tables_with_oids(ClusterInfo *cluster)
+{
+	UpgradeTaskReport report;
+	UpgradeTask *task = upgrade_task_create();
+	const char *query = "SELECT n.nspname, c.relname "
+		"FROM   pg_catalog.pg_class c, "
+		"       pg_catalog.pg_namespace n "
+		"WHERE  c.relnamespace = n.oid AND "
+		"       c.relhasoids AND"
+		"       n.nspname NOT IN ('pg_catalog')";
 
-		PQfinish(conn);
-	}
+	prep_status("Checking for tables WITH OIDS");
 
-	if (script)
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
+			 log_opts.basedir,
+			 "tables_with_oids.txt");
+
+	upgrade_task_add_step(task, query, process_with_oids_check,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
+
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains tables declared WITH OIDS, which is not\n"
 				 "supported anymore.  Consider removing the oid column using\n"
 				 "    ALTER TABLE ... SET WITHOUT OIDS;\n"
 				 "A list of tables with the problem is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v10-0011-Use-pg_upgrade-s-parallel-framework-for-encoding.patchtext/plain; charset=us-asciiDownload
From 3cd66a2e1bbe3fc7a90c96962c158475f715245d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 15:35:31 -0500
Subject: [PATCH v10 11/11] Use pg_upgrade's parallel framework for encoding
 conversion check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 120 ++++++++++++++++++++-----------------
 1 file changed, 64 insertions(+), 56 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index dff440b29a..01ab3d0694 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1684,81 +1684,89 @@ check_for_pg_role_prefix(ClusterInfo *cluster)
 }
 
 /*
- * Verify that no user-defined encoding conversions exist.
+ * Callback function for processing results of query for
+ * check_for_user_defined_encoding_conversions()'s UpgradeTask.  If the query
+ * returned any rows (i.e., the check failed), write the details to the report
+ * file.
  */
 static void
-check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
+process_user_defined_encoding_conversions(DbInfo *dbinfo, PGresult *res, void *arg)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_conoid = PQfnumber(res, "conoid");
+	int			i_conname = PQfnumber(res, "conname");
+	int			i_nspname = PQfnumber(res, "nspname");
 
-	prep_status("Checking for user-defined encoding conversions");
+	AssertVariableIsOfType(&process_user_defined_encoding_conversions,
+						   UpgradeTaskProcessCB);
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
-			 log_opts.basedir,
-			 "encoding_conversions.txt");
+	if (!ntups)
+		return;
 
-	/* Find any user defined encoding conversions */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int rowno = 0; rowno < ntups; rowno++)
 	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_conoid,
-					i_conname,
-					i_nspname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT c.oid as conoid, c.conname, n.nspname "
-								"FROM pg_catalog.pg_conversion c, "
-								"     pg_catalog.pg_namespace n "
-								"WHERE c.connamespace = n.oid AND "
-								"      c.oid >= 16384");
-		ntups = PQntuples(res);
-		i_conoid = PQfnumber(res, "conoid");
-		i_conname = PQfnumber(res, "conname");
-		i_nspname = PQfnumber(res, "nspname");
-		for (rowno = 0; rowno < ntups; rowno++)
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
 		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s\n",
-					PQgetvalue(res, rowno, i_conoid),
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_conname));
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
 		}
+		fprintf(report->file, "  (oid=%s) %s.%s\n",
+				PQgetvalue(res, rowno, i_conoid),
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_conname));
+	}
+}
 
-		PQclear(res);
+/*
+ * Verify that no user-defined encoding conversions exist.
+ */
+static void
+check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
+{
+	UpgradeTaskReport report;
+	UpgradeTask *task = upgrade_task_create();
+	const char *query;
 
-		PQfinish(conn);
-	}
+	prep_status("Checking for user-defined encoding conversions");
 
-	if (script)
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
+			 log_opts.basedir,
+			 "encoding_conversions.txt");
+
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	query = "SELECT c.oid as conoid, c.conname, n.nspname "
+		"FROM pg_catalog.pg_conversion c, "
+		"     pg_catalog.pg_namespace n "
+		"WHERE c.connamespace = n.oid AND "
+		"      c.oid >= 16384";
+
+	upgrade_task_add_step(task, query,
+						  process_user_defined_encoding_conversions,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
+
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains user-defined encoding conversions.\n"
 				 "The conversion function parameters changed in PostgreSQL version 14\n"
 				 "so this cluster cannot currently be upgraded.  You can remove the\n"
 				 "encoding conversions in the old cluster and restart the upgrade.\n"
 				 "A list of user-defined encoding conversions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

#29Ilya Gladyshev
ilya.v.gladyshev@gmail.com
In reply to: Nathan Bossart (#28)
Re: optimizing pg_upgrade's once-in-each-database steps

LGTM in general, but here are some final nitpicks:

+	if (maxFd != 0)
+		(void) select(maxFd + 1, &input_mask, &output_mask, &except_mask, NULL);

It’s a good idea to check for the return value of select, in case it returns any errors.

+			dbs_complete++;
+			(void) PQgetResult(slot->conn);
+			PQfinish(slot->conn);

Perhaps it’s rather for me to understand, what does PQgetResult call do here?

+			/* Check for connection failure. */
+			if (PQconnectPoll(slot->conn) == PGRES_POLLING_FAILED)
+				pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+
+			/* Check whether the connection is still establishing. */
+			if (PQconnectPoll(slot->conn) != PGRES_POLLING_OK)
+				return;

Are the two consecutive calls of PQconnectPoll intended here? Seems a bit wasteful, but maybe that’s ok.

We should probably mention this change in the docs as well, I found these two places that I think can be improved:

diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml
index 9877f2f01c..ad7aa33f07 100644
--- a/doc/src/sgml/ref/pgupgrade.sgml
+++ b/doc/src/sgml/ref/pgupgrade.sgml
@@ -118,7 +118,7 @@ PostgreSQL documentation
      <varlistentry>
       <term><option>-j <replaceable class="parameter">njobs</replaceable></option></term>
       <term><option>--jobs=<replaceable class="parameter">njobs</replaceable></option></term>
-      <listitem><para>number of simultaneous processes or threads to use
+      <listitem><para>number of simultaneous processes, threads or connections to use
       </para></listitem>
      </varlistentry>

@@ -587,8 +587,9 @@ NET STOP postgresql-&majorversion;

     <para>
      The <option>--jobs</option> option allows multiple CPU cores to be used
-     for copying/linking of files and to dump and restore database schemas
-     in parallel;  a good place to start is the maximum of the number of
+     for copying/linking of files, to dump and restore database schemas in
+     parallel and to use multiple simultaneous connections for upgrade checks;
+      a good place to start is the maximum of the number of
      CPU cores and tablespaces.  This option can dramatically reduce the
      time to upgrade a multi-database server running on a multiprocessor
      machine.
-- 
Show quoted text

28 авг. 2024 г., в 22:46, Nathan Bossart <nathandbossart@gmail.com> написал(а):

I spent some time cleaning up names, comments, etc. Barring additional
feedback, I'm planning to commit this stuff in the September commitfest so
that it has plenty of time to bake in the buildfarm.

--
nathan
<v10-0001-Introduce-framework-for-parallelizing-various-pg.patch><v10-0002-Use-pg_upgrade-s-new-parallel-framework-for-subs.patch><v10-0003-Use-pg_upgrade-s-new-parallel-framework-to-get-r.patch><v10-0004-Use-pg_upgrade-s-new-parallel-framework-to-get-l.patch><v10-0005-Use-pg_upgrade-s-new-parallel-framework-for-exte.patch><v10-0006-Use-pg_upgrade-s-new-parallel-framework-for-data.patch><v10-0007-Use-pg_upgrade-s-new-parallel-framework-for-isn-.patch><v10-0008-Use-pg_upgrade-s-new-parallel-framework-for-post.patch><v10-0009-Use-pg_upgrade-s-new-parallel-framework-for-poly.patch><v10-0010-Use-pg_upgrade-s-new-parallel-framework-for-WITH.patch><v10-0011-Use-pg_upgrade-s-parallel-framework-for-encoding.patch>

#30Nathan Bossart
nathandbossart@gmail.com
In reply to: Ilya Gladyshev (#29)
11 attachment(s)
Re: optimizing pg_upgrade's once-in-each-database steps

On Sat, Aug 31, 2024 at 01:18:10AM +0100, Ilya Gladyshev wrote:

LGTM in general, but here are some final nitpicks:

Thanks for reviewing.

+	if (maxFd != 0)
+		(void) select(maxFd + 1, &input_mask, &output_mask, &except_mask, NULL);

It�s a good idea to check for the return value of select, in case it
returns any errors.

Done.

+			dbs_complete++;
+			(void) PQgetResult(slot->conn);
+			PQfinish(slot->conn);

Perhaps it�s rather for me to understand, what does PQgetResult call do
here?

I believe I was trying to follow the guidance that you should always call
PQgetResult() until it returns NULL, but looking again, I don't see any
need to call it since we free the connection immediately afterwards.

+			/* Check for connection failure. */
+			if (PQconnectPoll(slot->conn) == PGRES_POLLING_FAILED)
+				pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+
+			/* Check whether the connection is still establishing. */
+			if (PQconnectPoll(slot->conn) != PGRES_POLLING_OK)
+				return;

Are the two consecutive calls of PQconnectPoll intended here? Seems a bit
wasteful, but maybe that�s ok.

I think we can actually just use PQstatus() here. But furthermore, I think
the way I was initiating connections was completely bogus. IIUC before
calling PQconnectPoll() the first time, we should wait for a write
indicator from select(), and then we should only call PQconnectPoll() after
subsequent indicators from select(). After spending quite a bit of time
staring at the PQconnectPoll() code, I'm quite surprised I haven't seen any
problems thus far. If I had to guess, I'd say that either PQconnectPoll()
is more resilient than I think it is, or I've just gotten lucky because
pg_upgrade establishes connections quickly.

Anyway, to fix this, I've added some more fields to the slot struct to
keep track of the information we need to properly establish connections,
and we now pay careful attention to the return value of select() so that we
know which slots are ready for processing. This seemed like a nice little
optimization independent of fixing connection establishment. I was worried
this was going to require a lot more code, but I think it only added ~50
lines or so.

We should probably mention this change in the docs as well, I found these
two places that I think can be improved:

I've adjusted the documentation in v11.

--
nathan

Attachments:

v11-0001-Introduce-framework-for-parallelizing-various-pg.patchtext/plain; charset=us-asciiDownload
From 02a9eb5bb035f5fdaf1c165161d96695dec06d45 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 10:45:59 -0500
Subject: [PATCH v11 01/11] Introduce framework for parallelizing various
 pg_upgrade tasks.

A number of pg_upgrade steps require connecting to every database
in the cluster and running the same query in each one.  When there
are many databases, these steps are particularly time-consuming,
especially since these steps are performed sequentially in a single
process.

This commit introduces a new framework that makes it easy to
parallelize most of these once-in-each-database tasks.
Specifically, it manages a simple state machine of slots and uses
libpq's asynchronous APIs to establish the connections and run the
queries.  The --jobs option is used to determine the number of
slots to use.  To use this new task framework, callers simply need
to provide the query and a callback function to process its
results, and the framework takes care of the rest.  A more complete
description is provided at the top of the new task.c file.

None of the eligible once-in-each-database tasks are converted to
use this new framework in this commit.  That will be done via
several follow-up commits.

Reviewed-by: Jeff Davis, Robert Haas, Daniel Gustafsson, Ilya Gladyshev, Corey Huinker
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 doc/src/sgml/ref/pgupgrade.sgml  |   6 +-
 src/bin/pg_upgrade/Makefile      |   1 +
 src/bin/pg_upgrade/meson.build   |   1 +
 src/bin/pg_upgrade/pg_upgrade.h  |  21 ++
 src/bin/pg_upgrade/task.c        | 491 +++++++++++++++++++++++++++++++
 src/tools/pgindent/typedefs.list |   5 +
 6 files changed, 522 insertions(+), 3 deletions(-)
 create mode 100644 src/bin/pg_upgrade/task.c

diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml
index 9877f2f01c..fc2d0ff845 100644
--- a/doc/src/sgml/ref/pgupgrade.sgml
+++ b/doc/src/sgml/ref/pgupgrade.sgml
@@ -118,7 +118,7 @@ PostgreSQL documentation
      <varlistentry>
       <term><option>-j <replaceable class="parameter">njobs</replaceable></option></term>
       <term><option>--jobs=<replaceable class="parameter">njobs</replaceable></option></term>
-      <listitem><para>number of simultaneous processes or threads to use
+      <listitem><para>number of simultaneous connections and processes/threads to use
       </para></listitem>
      </varlistentry>
 
@@ -587,8 +587,8 @@ NET STOP postgresql-&majorversion;
 
     <para>
      The <option>--jobs</option> option allows multiple CPU cores to be used
-     for copying/linking of files and to dump and restore database schemas
-     in parallel;  a good place to start is the maximum of the number of
+     for copying/linking of files, dumping and restoring database schemas
+     in parallel, etc.;  a good place to start is the maximum of the number of
      CPU cores and tablespaces.  This option can dramatically reduce the
      time to upgrade a multi-database server running on a multiprocessor
      machine.
diff --git a/src/bin/pg_upgrade/Makefile b/src/bin/pg_upgrade/Makefile
index bde91e2beb..f83d2b5d30 100644
--- a/src/bin/pg_upgrade/Makefile
+++ b/src/bin/pg_upgrade/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	relfilenumber.o \
 	server.o \
 	tablespace.o \
+	task.o \
 	util.o \
 	version.o
 
diff --git a/src/bin/pg_upgrade/meson.build b/src/bin/pg_upgrade/meson.build
index 9825fa3305..3d88419674 100644
--- a/src/bin/pg_upgrade/meson.build
+++ b/src/bin/pg_upgrade/meson.build
@@ -14,6 +14,7 @@ pg_upgrade_sources = files(
   'relfilenumber.c',
   'server.c',
   'tablespace.c',
+  'task.c',
   'util.c',
   'version.c',
 )
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index cdb6e2b759..53f693c2d4 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -494,3 +494,24 @@ void		parallel_transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr
 										  char *old_pgdata, char *new_pgdata,
 										  char *old_tablespace);
 bool		reap_child(bool wait_for_child);
+
+/* task.c */
+
+typedef void (*UpgradeTaskProcessCB) (DbInfo *dbinfo, PGresult *res, void *arg);
+
+/* struct definition is private to task.c */
+typedef struct UpgradeTask UpgradeTask;
+
+UpgradeTask *upgrade_task_create(void);
+void		upgrade_task_add_step(UpgradeTask *task, const char *query,
+								  UpgradeTaskProcessCB process_cb, bool free_result,
+								  void *arg);
+void		upgrade_task_run(const UpgradeTask *task, const ClusterInfo *cluster);
+void		upgrade_task_free(UpgradeTask *task);
+
+/* convenient type for common private data needed by several tasks */
+typedef struct
+{
+	FILE	   *file;
+	char		path[MAXPGPATH];
+} UpgradeTaskReport;
diff --git a/src/bin/pg_upgrade/task.c b/src/bin/pg_upgrade/task.c
new file mode 100644
index 0000000000..99d9b9d118
--- /dev/null
+++ b/src/bin/pg_upgrade/task.c
@@ -0,0 +1,491 @@
+/*
+ * task.c
+ *		framework for parallelizing pg_upgrade's once-in-each-database tasks
+ *
+ * This framework provides an efficient way of running the various
+ * once-in-each-database tasks required by pg_upgrade.  Specifically, it
+ * parallelizes these tasks by managing a simple state machine of
+ * user_opts.jobs slots and using libpq's asynchronous APIs to establish the
+ * connections and run the queries.  Callers simply need to create a callback
+ * function and build/execute an UpgradeTask.  A simple example follows:
+ *
+ *		static void
+ *		my_process_cb(DbInfo *dbinfo, PGresult *res, void *arg)
+ *		{
+ *			for (int i = 0; i < PQntuples(res); i++)
+ *			{
+ *				... process results ...
+ *			}
+ *		}
+ *
+ *		void
+ *		my_task(ClusterInfo *cluster)
+ *		{
+ *			UpgradeTask *task = upgrade_task_create();
+ *
+ *			upgrade_task_add_step(task,
+ *								  "... query text ...",
+ *								  my_process_cb,
+ *								  true,		// let the task free the PGresult
+ *								  NULL);	// "arg" pointer for callback
+ *			upgrade_task_run(task, cluster);
+ *			upgrade_task_free(task);
+ *		}
+ *
+ * Note that multiple steps can be added to a given task.  When there are
+ * multiple steps, the task will run all of the steps consecutively in the same
+ * database connection before freeing the connection and moving on.  In other
+ * words, it only ever initiates one connection to each database in the
+ * cluster for a given run.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ * src/bin/pg_upgrade/task.c
+ */
+
+#include "postgres_fe.h"
+
+#include "common/connect.h"
+#include "fe_utils/string_utils.h"
+#include "pg_upgrade.h"
+
+/*
+ * dbs_complete stores the number of databases that we have completed
+ * processing.  When this value equals the number of databases in the cluster,
+ * the task is finished.
+ */
+static int	dbs_complete;
+
+/*
+ * dbs_processing stores the index of the next database in the cluster's array
+ * of databases that will be picked up for processing.  It will always be
+ * greater than or equal to dbs_complete.
+ */
+static int	dbs_processing;
+
+/*
+ * This struct stores all the information for a single step of a task.  All
+ * steps in a task are run in a single connection before moving on to the next
+ * database (which requires a new connection).
+ */
+typedef struct UpgradeTaskStep
+{
+	UpgradeTaskProcessCB process_cb;	/* processes the results of the query */
+	const char *query;			/* query text */
+	bool		free_result;	/* should we free the result? */
+	void	   *arg;			/* pointer passed to process_cb */
+} UpgradeTaskStep;
+
+/*
+ * This struct is a thin wrapper around an array of steps, i.e.,
+ * UpgradeTaskStep.
+ */
+typedef struct UpgradeTask
+{
+	UpgradeTaskStep *steps;
+	int			num_steps;
+} UpgradeTask;
+
+/*
+ * The different states for a parallel slot.
+ */
+typedef enum
+{
+	FREE,						/* slot available for use in a new database */
+	CONNECTING,					/* waiting for connection to be established */
+	RUNNING_QUERIES,			/* running/processing queries in the task */
+} UpgradeTaskSlotState;
+
+/*
+ * We maintain an array of user_opts.jobs slots to execute the task.
+ */
+typedef struct
+{
+	UpgradeTaskSlotState state; /* state of the slot */
+	int			db_idx;			/* index of the database assigned to slot */
+	int			step_idx;		/* index of the current step of task */
+	PGconn	   *conn;			/* current connection managed by slot */
+	bool		ready;			/* slot is ready for processing */
+	bool		select_mode;	/* select() mode: true->read, false->write */
+	int			sock;			/* file descriptor for connection's socket */
+} UpgradeTaskSlot;
+
+/*
+ * Initializes an UpgradeTask.
+ */
+UpgradeTask *
+upgrade_task_create(void)
+{
+	UpgradeTask *task = pg_malloc0(sizeof(UpgradeTask));
+
+	/* All tasks must first set a secure search_path. */
+	upgrade_task_add_step(task, ALWAYS_SECURE_SEARCH_PATH_SQL, NULL, true, NULL);
+	return task;
+}
+
+/*
+ * Frees all storage associated with an UpgradeTask.
+ */
+void
+upgrade_task_free(UpgradeTask *task)
+{
+	if (task->steps)
+		pg_free(task->steps);
+
+	pg_free(task);
+}
+
+/*
+ * Adds a step to an UpgradeTask.  The steps will be executed in each database
+ * in the order in which they are added.
+ *
+ *	task: task object that must have been initialized via upgrade_task_create()
+ *	query: the query text
+ *	process_cb: function that processes the results of the query
+ *	free_result: should we free the PGresult, or leave it to the caller?
+ *	arg: pointer to task-specific data that is passed to each callback
+ */
+void
+upgrade_task_add_step(UpgradeTask *task, const char *query,
+					  UpgradeTaskProcessCB process_cb, bool free_result,
+					  void *arg)
+{
+	UpgradeTaskStep *new_step;
+
+	task->steps = pg_realloc(task->steps,
+							 ++task->num_steps * sizeof(UpgradeTaskStep));
+
+	new_step = &task->steps[task->num_steps - 1];
+	new_step->process_cb = process_cb;
+	new_step->query = query;
+	new_step->free_result = free_result;
+	new_step->arg = arg;
+}
+
+/*
+ * Build a connection string for the slot's current database and asynchronously
+ * start a new connection, but do not wait for the connection to be
+ * established.
+ */
+static void
+start_conn(const ClusterInfo *cluster, UpgradeTaskSlot *slot)
+{
+	PQExpBufferData conn_opts;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db_idx];
+
+	/* Build connection string with proper quoting */
+	initPQExpBuffer(&conn_opts);
+	appendPQExpBufferStr(&conn_opts, "dbname=");
+	appendConnStrVal(&conn_opts, dbinfo->db_name);
+	appendPQExpBufferStr(&conn_opts, " user=");
+	appendConnStrVal(&conn_opts, os_info.user);
+	appendPQExpBuffer(&conn_opts, " port=%d", cluster->port);
+	if (cluster->sockdir)
+	{
+		appendPQExpBufferStr(&conn_opts, " host=");
+		appendConnStrVal(&conn_opts, cluster->sockdir);
+	}
+
+	slot->conn = PQconnectStart(conn_opts.data);
+
+	if (!slot->conn)
+		pg_fatal("failed to create connection with connection string: \"%s\"",
+				 conn_opts.data);
+
+	termPQExpBuffer(&conn_opts);
+}
+
+/*
+ * Run the process_cb callback function to process the result of a query, and
+ * free the result if the caller indicated we should do so.
+ */
+static void
+process_query_result(const ClusterInfo *cluster, UpgradeTaskSlot *slot,
+					 const UpgradeTask *task)
+{
+	UpgradeTaskStep *steps = &task->steps[slot->step_idx];
+	UpgradeTaskProcessCB process_cb = steps->process_cb;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db_idx];
+	PGresult   *res = PQgetResult(slot->conn);
+
+	if (PQstatus(slot->conn) == CONNECTION_BAD ||
+		(PQresultStatus(res) != PGRES_TUPLES_OK &&
+		 PQresultStatus(res) != PGRES_COMMAND_OK))
+		pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+
+	/*
+	 * We assume that a NULL process_cb callback function means there's
+	 * nothing to process.  This is primarily intended for the inital step in
+	 * every task that sets a safe search_path.
+	 */
+	if (process_cb)
+		(*process_cb) (dbinfo, res, steps->arg);
+
+	if (steps->free_result)
+		PQclear(res);
+}
+
+/*
+ * Advances the state machine for a given slot as necessary.
+ */
+static void
+process_slot(const ClusterInfo *cluster, UpgradeTaskSlot *slot, const UpgradeTask *task)
+{
+	if (!slot->ready)
+		return;
+
+	switch (slot->state)
+	{
+		case FREE:
+
+			/*
+			 * If all of the databases in the cluster have been processed or
+			 * are currently being processed by other slots, we are done.
+			 */
+			if (dbs_processing >= cluster->dbarr.ndbs)
+				return;
+
+			/*
+			 * Claim the next database in the cluster's array and initiate a
+			 * new connection.
+			 */
+			slot->db_idx = dbs_processing++;
+			slot->state = CONNECTING;
+			start_conn(cluster, slot);
+
+			return;
+
+		case CONNECTING:
+
+			{
+				ConnStatusType status = PQstatus(slot->conn);
+
+				/* Check for connection failure. */
+				if (status == CONNECTION_BAD)
+					pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+
+				/* Check whether the connection is still establishing. */
+				if (status != CONNECTION_OK)
+					return;
+			}
+
+			/*
+			 * Move on to running/processing the queries in the task.  We
+			 * combine all the queries and send them to the server together.
+			 */
+			slot->state = RUNNING_QUERIES;
+
+			{
+				PQExpBuffer queries = createPQExpBuffer();
+
+				for (int i = 0; i < task->num_steps; i++)
+					appendPQExpBuffer(queries, "%s;", task->steps[i].query);
+				if (!PQsendQuery(slot->conn, queries->data))
+					pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+				destroyPQExpBuffer(queries);
+			}
+
+			return;
+
+		case RUNNING_QUERIES:
+
+			/*
+			 * Consume any available data and clear the read-ready indicator
+			 * for the connection.
+			 */
+			if (!PQconsumeInput(slot->conn))
+				pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+
+			/*
+			 * Process any results that are ready so that we can free up this
+			 * slot for another database as soon as possible.
+			 */
+			for (; slot->step_idx < task->num_steps; slot->step_idx++)
+			{
+				/* If no more results are available yet, move on. */
+				if (PQisBusy(slot->conn))
+					return;
+
+				process_query_result(cluster, slot, task);
+			}
+
+			/*
+			 * If we just finished processing the result of the last step in
+			 * the task, free the slot.  We recursively call this function on
+			 * the newly-freed slot so that we can start initiating the next
+			 * connection immediately instead of waiting for the next loop
+			 * through the slots.
+			 */
+			dbs_complete++;
+			PQfinish(slot->conn);
+			memset(slot, 0, sizeof(UpgradeTaskSlot));
+			slot->ready = true;
+
+			process_slot(cluster, slot, task);
+
+			return;
+	}
+}
+
+/*
+ * Returns -1 on error, else the number of ready descriptors.
+ */
+static int
+select_loop(int maxFd, fd_set *input, fd_set *output, bool nowait)
+{
+	fd_set		save_input = *input;
+	fd_set		save_output = *output;
+	struct timeval timeout = {0, 0};
+
+	if (maxFd == 0)
+		return 0;
+
+	for (;;)
+	{
+		int			i;
+
+		*input = save_input;
+		*output = save_output;
+
+		i = select(maxFd + 1, input, output, NULL, nowait ? &timeout : NULL);
+
+#ifndef WIN32
+		if (i < 0 && errno == EINTR)
+			continue;
+#else
+		if (i == SOCKET_ERROR && WSAGetLastError() == WSAEINTR)
+			continue;
+#endif
+		return i;
+	}
+}
+
+/*
+ * Wait on the slots to either finish connecting or to receive query results if
+ * possible.  This avoids a tight loop in upgrade_task_run().
+ */
+static void
+wait_on_slots(UpgradeTaskSlot *slots, int numslots)
+{
+	fd_set		input;
+	fd_set		output;
+	int			maxFd = 0;
+	bool		skip_wait = false;
+
+	FD_ZERO(&input);
+	FD_ZERO(&output);
+
+	for (int i = 0; i < numslots; i++)
+	{
+		switch (slots[i].state)
+		{
+			case FREE:
+
+				/*
+				 * This function should only ever see free slots as we are
+				 * finishing processing the last few databases, at which point
+				 * we don't have any databases left for them to process. We'll
+				 * never use these slots again, so we can safely ignore them.
+				 */
+				slots[i].ready = false;
+				continue;
+
+			case CONNECTING:
+
+				/*
+				 * Don't call PQconnectPoll() again for this slot until
+				 * select() tells us something is ready.  Be sure to use the
+				 * previous poll mode in this case.
+				 */
+				if (!slots[i].ready)
+					break;
+
+				/*
+				 * If we are waiting for the connection to establish, choose
+				 * whether to wait for reading or for writing on the socket as
+				 * appropriate.  If neither apply, mark the slot as ready and
+				 * skip waiting so that it is handled ASAP (we assume this
+				 * means the connection is either bad or fully ready).
+				 */
+				{
+					PostgresPollingStatusType status;
+
+					status = PQconnectPoll(slots[i].conn);
+					if (status == PGRES_POLLING_READING)
+						slots[i].select_mode = true;
+					else if (status == PGRES_POLLING_WRITING)
+						slots[i].select_mode = false;
+					else
+					{
+						slots[i].ready = true;
+						skip_wait = true;
+						continue;
+					}
+				}
+
+				break;
+
+			case RUNNING_QUERIES:
+
+				/*
+				 * Once we've sent the queries, we must wait for the socket to
+				 * be read-ready.  Note that process_slot() handles calling
+				 * PQconsumeInput() as required.
+				 */
+				slots[i].select_mode = true;
+				break;
+		}
+
+		/*
+		 * Add the socket to the set.
+		 */
+		slots[i].ready = false;
+		slots[i].sock = PQsocket(slots[i].conn);
+		if (slots[i].sock < 0)
+			pg_fatal("invalid socket");
+		FD_SET(slots[i].sock, slots[i].select_mode ? &input : &output);
+		maxFd = Max(maxFd, slots[i].sock);
+	}
+
+	/*
+	 * If we found socket(s) to wait on, wait.
+	 */
+	if (select_loop(maxFd, &input, &output, skip_wait) == -1)
+		pg_fatal("select() failed: %m");
+
+	/*
+	 * Mark which sockets appear to be ready.
+	 */
+	for (int i = 0; i < numslots; i++)
+		slots[i].ready |= (FD_ISSET(slots[i].sock, &input) ||
+						   FD_ISSET(slots[i].sock, &output));
+}
+
+/*
+ * Runs all the steps of the task in every database in the cluster using
+ * user_opts.jobs parallel slots.
+ */
+void
+upgrade_task_run(const UpgradeTask *task, const ClusterInfo *cluster)
+{
+	int			jobs = Max(1, user_opts.jobs);
+	UpgradeTaskSlot *slots = pg_malloc0(sizeof(UpgradeTaskSlot) * jobs);
+
+	dbs_complete = 0;
+	dbs_processing = 0;
+
+	/*
+	 * Process every slot the first time round.
+	 */
+	for (int i = 0; i < jobs; i++)
+		slots[i].ready = true;
+
+	while (dbs_complete < cluster->dbarr.ndbs)
+	{
+		for (int i = 0; i < jobs; i++)
+			process_slot(cluster, &slots[i], task);
+
+		wait_on_slots(slots, jobs);
+	}
+
+	pg_free(slots);
+}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9e951a9e6f..43c0f9f85b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3039,6 +3039,11 @@ UnresolvedTup
 UnresolvedTupData
 UpdateContext
 UpdateStmt
+UpgradeTask
+UpgradeTaskReport
+UpgradeTaskSlot
+UpgradeTaskSlotState
+UpgradeTaskStep
 UploadManifestCmd
 UpperRelationKind
 UpperUniquePath
-- 
2.39.3 (Apple Git-146)

v11-0002-Use-pg_upgrade-s-new-parallel-framework-for-subs.patchtext/plain; charset=us-asciiDownload
From 07aca3723cbc027690ef383a25fd2e3f2020b934 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 17:21:19 -0500
Subject: [PATCH v11 02/11] Use pg_upgrade's new parallel framework for
 subscription checks.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 206 ++++++++++++++++++++-----------------
 1 file changed, 111 insertions(+), 95 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 96adea41e9..f8160e0140 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1905,6 +1905,38 @@ check_old_cluster_for_valid_slots(void)
 	check_ok();
 }
 
+/*
+ * Callback function for processing results of query for
+ * check_old_cluster_subscription_state()'s UpgradeTask.  If the query returned
+ * any rows (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_old_sub_state_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	int			ntup = PQntuples(res);
+	int			i_srsubstate = PQfnumber(res, "srsubstate");
+	int			i_subname = PQfnumber(res, "subname");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+
+	AssertVariableIsOfType(&process_old_sub_state_check, UpgradeTaskProcessCB);
+
+	for (int i = 0; i < ntup; i++)
+	{
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+
+		fprintf(report->file, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
+				PQgetvalue(res, i, i_srsubstate),
+				dbinfo->db_name,
+				PQgetvalue(res, i, i_subname),
+				PQgetvalue(res, i, i_nspname),
+				PQgetvalue(res, i, i_relname));
+	}
+}
+
 /*
  * check_old_cluster_subscription_state()
  *
@@ -1915,115 +1947,99 @@ check_old_cluster_for_valid_slots(void)
 static void
 check_old_cluster_subscription_state(void)
 {
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTask *task = upgrade_task_create();
+	UpgradeTaskReport report;
+	const char *query;
+	PGresult   *res;
+	PGconn	   *conn;
 	int			ntup;
 
 	prep_status("Checking for subscription state");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
 			 log_opts.basedir,
 			 "subs_invalid.txt");
-	for (int dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
-
-		/* We need to check for pg_replication_origin only once. */
-		if (dbnum == 0)
-		{
-			/*
-			 * Check that all the subscriptions have their respective
-			 * replication origin.
-			 */
-			res = executeQueryOrDie(conn,
-									"SELECT d.datname, s.subname "
-									"FROM pg_catalog.pg_subscription s "
-									"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
-									"	ON o.roname = 'pg_' || s.oid "
-									"INNER JOIN pg_catalog.pg_database d "
-									"	ON d.oid = s.subdbid "
-									"WHERE o.roname IS NULL;");
-
-			ntup = PQntuples(res);
-			for (int i = 0; i < ntup; i++)
-			{
-				if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-					pg_fatal("could not open file \"%s\": %m", output_path);
-				fprintf(script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
-						PQgetvalue(res, i, 0),
-						PQgetvalue(res, i, 1));
-			}
-			PQclear(res);
-		}
-
-		/*
-		 * We don't allow upgrade if there is a risk of dangling slot or
-		 * origin corresponding to initial sync after upgrade.
-		 *
-		 * A slot/origin not created yet refers to the 'i' (initialize) state,
-		 * while 'r' (ready) state refers to a slot/origin created previously
-		 * but already dropped. These states are supported for pg_upgrade. The
-		 * other states listed below are not supported:
-		 *
-		 * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state
-		 * would retain a replication slot, which could not be dropped by the
-		 * sync worker spawned after the upgrade because the subscription ID
-		 * used for the slot name won't match anymore.
-		 *
-		 * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state
-		 * would retain the replication origin when there is a failure in
-		 * tablesync worker immediately after dropping the replication slot in
-		 * the publisher.
-		 *
-		 * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on
-		 * a relation upgraded while in this state would expect an origin ID
-		 * with the OID of the subscription used before the upgrade, causing
-		 * it to fail.
-		 *
-		 * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and
-		 * SUBREL_STATE_UNKNOWN: These states are not stored in the catalog,
-		 * so we need not allow these states.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT r.srsubstate, s.subname, n.nspname, c.relname "
-								"FROM pg_catalog.pg_subscription_rel r "
-								"LEFT JOIN pg_catalog.pg_subscription s"
-								"	ON r.srsubid = s.oid "
-								"LEFT JOIN pg_catalog.pg_class c"
-								"	ON r.srrelid = c.oid "
-								"LEFT JOIN pg_catalog.pg_namespace n"
-								"	ON c.relnamespace = n.oid "
-								"WHERE r.srsubstate NOT IN ('i', 'r') "
-								"ORDER BY s.subname");
-
-		ntup = PQntuples(res);
-		for (int i = 0; i < ntup; i++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-
-			fprintf(script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
-					PQgetvalue(res, i, 0),
-					active_db->db_name,
-					PQgetvalue(res, i, 1),
-					PQgetvalue(res, i, 2),
-					PQgetvalue(res, i, 3));
-		}
 
-		PQclear(res);
-		PQfinish(conn);
+	/*
+	 * Check that all the subscriptions have their respective replication
+	 * origin.  This check only needs to run once.
+	 */
+	conn = connectToServer(&old_cluster, old_cluster.dbarr.dbs[0].db_name);
+	res = executeQueryOrDie(conn,
+							"SELECT d.datname, s.subname "
+							"FROM pg_catalog.pg_subscription s "
+							"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
+							"	ON o.roname = 'pg_' || s.oid "
+							"INNER JOIN pg_catalog.pg_database d "
+							"	ON d.oid = s.subdbid "
+							"WHERE o.roname IS NULL;");
+	ntup = PQntuples(res);
+	for (int i = 0; i < ntup; i++)
+	{
+		if (report.file == NULL &&
+			(report.file = fopen_priv(report.path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report.path);
+		fprintf(report.file, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
+				PQgetvalue(res, i, 0),
+				PQgetvalue(res, i, 1));
 	}
+	PQclear(res);
+	PQfinish(conn);
 
-	if (script)
+	/*
+	 * We don't allow upgrade if there is a risk of dangling slot or origin
+	 * corresponding to initial sync after upgrade.
+	 *
+	 * A slot/origin not created yet refers to the 'i' (initialize) state,
+	 * while 'r' (ready) state refers to a slot/origin created previously but
+	 * already dropped. These states are supported for pg_upgrade. The other
+	 * states listed below are not supported:
+	 *
+	 * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state would
+	 * retain a replication slot, which could not be dropped by the sync
+	 * worker spawned after the upgrade because the subscription ID used for
+	 * the slot name won't match anymore.
+	 *
+	 * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state would
+	 * retain the replication origin when there is a failure in tablesync
+	 * worker immediately after dropping the replication slot in the
+	 * publisher.
+	 *
+	 * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on a
+	 * relation upgraded while in this state would expect an origin ID with
+	 * the OID of the subscription used before the upgrade, causing it to
+	 * fail.
+	 *
+	 * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and
+	 * SUBREL_STATE_UNKNOWN: These states are not stored in the catalog, so we
+	 * need not allow these states.
+	 */
+	query = "SELECT r.srsubstate, s.subname, n.nspname, c.relname "
+		"FROM pg_catalog.pg_subscription_rel r "
+		"LEFT JOIN pg_catalog.pg_subscription s"
+		"   ON r.srsubid = s.oid "
+		"LEFT JOIN pg_catalog.pg_class c"
+		"   ON r.srrelid = c.oid "
+		"LEFT JOIN pg_catalog.pg_namespace n"
+		"   ON c.relnamespace = n.oid "
+		"WHERE r.srsubstate NOT IN ('i', 'r') "
+		"ORDER BY s.subname";
+
+	upgrade_task_add_step(task, query, process_old_sub_state_check,
+						  true, &report);
+
+	upgrade_task_run(task, &old_cluster);
+	upgrade_task_free(task);
+
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains subscriptions without origin or having relations not in i (initialize) or r (ready) state.\n"
 				 "You can allow the initial sync to finish for all relations and then restart the upgrade.\n"
 				 "A list of the problematic subscriptions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v11-0003-Use-pg_upgrade-s-new-parallel-framework-to-get-r.patchtext/plain; charset=us-asciiDownload
From 550115d651038aad3ef7c2fb277f3c5bbb071a82 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:09:33 -0500
Subject: [PATCH v11 03/11] Use pg_upgrade's new parallel framework to get
 relation info.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/info.c | 296 ++++++++++++++++++++------------------
 1 file changed, 154 insertions(+), 142 deletions(-)

diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index d3c1e8918d..2bfc8dcfba 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -11,6 +11,7 @@
 
 #include "access/transam.h"
 #include "catalog/pg_class_d.h"
+#include "pqexpbuffer.h"
 #include "pg_upgrade.h"
 
 static void create_rel_filename_map(const char *old_data, const char *new_data,
@@ -22,12 +23,14 @@ static void report_unmatched_relation(const RelInfo *rel, const DbInfo *db,
 static void free_db_and_rel_infos(DbInfoArr *db_arr);
 static void get_template0_info(ClusterInfo *cluster);
 static void get_db_infos(ClusterInfo *cluster);
-static void get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo);
+static char *get_rel_infos_query(void);
+static void process_rel_infos(DbInfo *dbinfo, PGresult *res, void *arg);
 static void free_rel_infos(RelInfoArr *rel_arr);
 static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
-static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo);
+static char *get_old_cluster_logical_slot_infos_query(void);
+static void process_old_cluster_logical_slot_infos(DbInfo *dbinfo, PGresult *res, void *arg);
 
 
 /*
@@ -276,7 +279,9 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
 void
 get_db_rel_and_slot_infos(ClusterInfo *cluster)
 {
-	int			dbnum;
+	UpgradeTask *task = upgrade_task_create();
+	char	   *rel_infos_query = NULL;
+	char	   *logical_slot_infos_query = NULL;
 
 	if (cluster->dbarr.dbs != NULL)
 		free_db_and_rel_infos(&cluster->dbarr);
@@ -284,15 +289,37 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster)
 	get_template0_info(cluster);
 	get_db_infos(cluster);
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	rel_infos_query = get_rel_infos_query();
+	upgrade_task_add_step(task,
+						  rel_infos_query,
+						  process_rel_infos,
+						  true, NULL);
+
+	/*
+	 * Logical slots are only carried over to the new cluster when the old
+	 * cluster is on PG17 or newer.  This is because before that the logical
+	 * slots are not saved at shutdown, so there is no guarantee that the
+	 * latest confirmed_flush_lsn is saved to disk which can lead to data
+	 * loss. It is still not guaranteed for manually created slots in PG17, so
+	 * subsequent checks done in check_old_cluster_for_valid_slots() would
+	 * raise a FATAL error if such slots are included.
+	 */
+	if (cluster == &old_cluster &&
+		GET_MAJOR_VERSION(cluster->major_version) > 1600)
 	{
-		DbInfo	   *pDbInfo = &cluster->dbarr.dbs[dbnum];
+		logical_slot_infos_query = get_old_cluster_logical_slot_infos_query();
+		upgrade_task_add_step(task,
+							  logical_slot_infos_query,
+							  process_old_cluster_logical_slot_infos,
+							  true, NULL);
+	}
 
-		get_rel_infos(cluster, pDbInfo);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
 
-		if (cluster == &old_cluster)
-			get_old_cluster_logical_slot_infos(pDbInfo);
-	}
+	pg_free(rel_infos_query);
+	if (logical_slot_infos_query)
+		pg_free(logical_slot_infos_query);
 
 	if (cluster == &old_cluster)
 		pg_log(PG_VERBOSE, "\nsource databases:");
@@ -431,40 +458,21 @@ get_db_infos(ClusterInfo *cluster)
 
 
 /*
- * get_rel_infos()
+ * get_rel_infos_query()
  *
- * gets the relinfos for all the user tables and indexes of the database
- * referred to by "dbinfo".
+ * Returns the query for retrieving the relation information for all the user
+ * tables and indexes in the database, for use by get_db_rel_and_slot_infos()'s
+ * UpgradeTask.
  *
- * Note: the resulting RelInfo array is assumed to be sorted by OID.
- * This allows later processing to match up old and new databases efficiently.
+ * Note: the result is assumed to be sorted by OID.  This allows later
+ * processing to match up old and new databases efficiently.
  */
-static void
-get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
+static char *
+get_rel_infos_query(void)
 {
-	PGconn	   *conn = connectToServer(cluster,
-									   dbinfo->db_name);
-	PGresult   *res;
-	RelInfo    *relinfos;
-	int			ntups;
-	int			relnum;
-	int			num_rels = 0;
-	char	   *nspname = NULL;
-	char	   *relname = NULL;
-	char	   *tablespace = NULL;
-	int			i_spclocation,
-				i_nspname,
-				i_relname,
-				i_reloid,
-				i_indtable,
-				i_toastheap,
-				i_relfilenumber,
-				i_reltablespace;
-	char		query[QUERY_ALLOC];
-	char	   *last_namespace = NULL,
-			   *last_tablespace = NULL;
+	PQExpBufferData query;
 
-	query[0] = '\0';			/* initialize query string to empty */
+	initPQExpBuffer(&query);
 
 	/*
 	 * Create a CTE that collects OIDs of regular user tables and matviews,
@@ -476,34 +484,34 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * output, so we have to copy that system table.  It's easiest to do that
 	 * by treating it as a user table.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "WITH regular_heap (reloid, indtable, toastheap) AS ( "
-			 "  SELECT c.oid, 0::oid, 0::oid "
-			 "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
-			 "         ON c.relnamespace = n.oid "
-			 "  WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
-			 CppAsString2(RELKIND_MATVIEW) ") AND "
+	appendPQExpBuffer(&query,
+					  "WITH regular_heap (reloid, indtable, toastheap) AS ( "
+					  "  SELECT c.oid, 0::oid, 0::oid "
+					  "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
+					  "         ON c.relnamespace = n.oid "
+					  "  WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
+					  CppAsString2(RELKIND_MATVIEW) ") AND "
 	/* exclude possible orphaned temp tables */
-			 "    ((n.nspname !~ '^pg_temp_' AND "
-			 "      n.nspname !~ '^pg_toast_temp_' AND "
-			 "      n.nspname NOT IN ('pg_catalog', 'information_schema', "
-			 "                        'binary_upgrade', 'pg_toast') AND "
-			 "      c.oid >= %u::pg_catalog.oid) OR "
-			 "     (n.nspname = 'pg_catalog' AND "
-			 "      relname IN ('pg_largeobject') ))), ",
-			 FirstNormalObjectId);
+					  "    ((n.nspname !~ '^pg_temp_' AND "
+					  "      n.nspname !~ '^pg_toast_temp_' AND "
+					  "      n.nspname NOT IN ('pg_catalog', 'information_schema', "
+					  "                        'binary_upgrade', 'pg_toast') AND "
+					  "      c.oid >= %u::pg_catalog.oid) OR "
+					  "     (n.nspname = 'pg_catalog' AND "
+					  "      relname IN ('pg_largeobject') ))), ",
+					  FirstNormalObjectId);
 
 	/*
 	 * Add a CTE that collects OIDs of toast tables belonging to the tables
 	 * selected by the regular_heap CTE.  (We have to do this separately
 	 * because the namespace-name rules above don't work for toast tables.)
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "  toast_heap (reloid, indtable, toastheap) AS ( "
-			 "  SELECT c.reltoastrelid, 0::oid, c.oid "
-			 "  FROM regular_heap JOIN pg_catalog.pg_class c "
-			 "      ON regular_heap.reloid = c.oid "
-			 "  WHERE c.reltoastrelid != 0), ");
+	appendPQExpBufferStr(&query,
+						 "  toast_heap (reloid, indtable, toastheap) AS ( "
+						 "  SELECT c.reltoastrelid, 0::oid, c.oid "
+						 "  FROM regular_heap JOIN pg_catalog.pg_class c "
+						 "      ON regular_heap.reloid = c.oid "
+						 "  WHERE c.reltoastrelid != 0), ");
 
 	/*
 	 * Add a CTE that collects OIDs of all valid indexes on the previously
@@ -511,53 +519,68 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * Testing indisready is necessary in 9.2, and harmless in earlier/later
 	 * versions.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "  all_index (reloid, indtable, toastheap) AS ( "
-			 "  SELECT indexrelid, indrelid, 0::oid "
-			 "  FROM pg_catalog.pg_index "
-			 "  WHERE indisvalid AND indisready "
-			 "    AND indrelid IN "
-			 "        (SELECT reloid FROM regular_heap "
-			 "         UNION ALL "
-			 "         SELECT reloid FROM toast_heap)) ");
+	appendPQExpBufferStr(&query,
+						 "  all_index (reloid, indtable, toastheap) AS ( "
+						 "  SELECT indexrelid, indrelid, 0::oid "
+						 "  FROM pg_catalog.pg_index "
+						 "  WHERE indisvalid AND indisready "
+						 "    AND indrelid IN "
+						 "        (SELECT reloid FROM regular_heap "
+						 "         UNION ALL "
+						 "         SELECT reloid FROM toast_heap)) ");
 
 	/*
 	 * And now we can write the query that retrieves the data we want for each
 	 * heap and index relation.  Make sure result is sorted by OID.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "SELECT all_rels.*, n.nspname, c.relname, "
-			 "  c.relfilenode, c.reltablespace, "
-			 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
-			 "FROM (SELECT * FROM regular_heap "
-			 "      UNION ALL "
-			 "      SELECT * FROM toast_heap "
-			 "      UNION ALL "
-			 "      SELECT * FROM all_index) all_rels "
-			 "  JOIN pg_catalog.pg_class c "
-			 "      ON all_rels.reloid = c.oid "
-			 "  JOIN pg_catalog.pg_namespace n "
-			 "     ON c.relnamespace = n.oid "
-			 "  LEFT OUTER JOIN pg_catalog.pg_tablespace t "
-			 "     ON c.reltablespace = t.oid "
-			 "ORDER BY 1;");
-
-	res = executeQueryOrDie(conn, "%s", query);
-
-	ntups = PQntuples(res);
+	appendPQExpBufferStr(&query,
+						 "SELECT all_rels.*, n.nspname, c.relname, "
+						 "  c.relfilenode, c.reltablespace, "
+						 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
+						 "FROM (SELECT * FROM regular_heap "
+						 "      UNION ALL "
+						 "      SELECT * FROM toast_heap "
+						 "      UNION ALL "
+						 "      SELECT * FROM all_index) all_rels "
+						 "  JOIN pg_catalog.pg_class c "
+						 "      ON all_rels.reloid = c.oid "
+						 "  JOIN pg_catalog.pg_namespace n "
+						 "     ON c.relnamespace = n.oid "
+						 "  LEFT OUTER JOIN pg_catalog.pg_tablespace t "
+						 "     ON c.reltablespace = t.oid "
+						 "ORDER BY 1;");
+
+	return query.data;
+}
 
-	relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+/*
+ * Callback function for processing results of the query returned by
+ * get_rel_infos_query(), which is used for get_db_rel_and_slot_infos()'s
+ * UpgradeTask.  This function stores the relation information for later use.
+ */
+static void
+process_rel_infos(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	int			ntups = PQntuples(res);
+	RelInfo    *relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	int			i_reloid = PQfnumber(res, "reloid");
+	int			i_indtable = PQfnumber(res, "indtable");
+	int			i_toastheap = PQfnumber(res, "toastheap");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+	int			i_relfilenumber = PQfnumber(res, "relfilenode");
+	int			i_reltablespace = PQfnumber(res, "reltablespace");
+	int			i_spclocation = PQfnumber(res, "spclocation");
+	int			num_rels = 0;
+	char	   *nspname = NULL;
+	char	   *relname = NULL;
+	char	   *tablespace = NULL;
+	char	   *last_namespace = NULL;
+	char	   *last_tablespace = NULL;
 
-	i_reloid = PQfnumber(res, "reloid");
-	i_indtable = PQfnumber(res, "indtable");
-	i_toastheap = PQfnumber(res, "toastheap");
-	i_nspname = PQfnumber(res, "nspname");
-	i_relname = PQfnumber(res, "relname");
-	i_relfilenumber = PQfnumber(res, "relfilenode");
-	i_reltablespace = PQfnumber(res, "reltablespace");
-	i_spclocation = PQfnumber(res, "spclocation");
+	AssertVariableIsOfType(&process_rel_infos, UpgradeTaskProcessCB);
 
-	for (relnum = 0; relnum < ntups; relnum++)
+	for (int relnum = 0; relnum < ntups; relnum++)
 	{
 		RelInfo    *curr = &relinfos[num_rels++];
 
@@ -610,44 +633,22 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 			/* A zero reltablespace oid indicates the database tablespace. */
 			curr->tablespace = dbinfo->db_tablespace;
 	}
-	PQclear(res);
-
-	PQfinish(conn);
 
 	dbinfo->rel_arr.rels = relinfos;
 	dbinfo->rel_arr.nrels = num_rels;
 }
 
 /*
- * get_old_cluster_logical_slot_infos()
- *
- * Gets the LogicalSlotInfos for all the logical replication slots of the
- * database referred to by "dbinfo". The status of each logical slot is gotten
- * here, but they are used at the checking phase. See
- * check_old_cluster_for_valid_slots().
+ * get_old_cluster_logical_slot_infos_query()
  *
- * Note: This function will not do anything if the old cluster is pre-PG17.
- * This is because before that the logical slots are not saved at shutdown, so
- * there is no guarantee that the latest confirmed_flush_lsn is saved to disk
- * which can lead to data loss. It is still not guaranteed for manually created
- * slots in PG17, so subsequent checks done in
- * check_old_cluster_for_valid_slots() would raise a FATAL error if such slots
- * are included.
+ * Returns the query for retrieving the logical slot information for all the
+ * logical replication slots in the database, for use by
+ * get_db_rel_and_slot_infos()'s UpgradeTask.  The status of each logical slot
+ * is checked in check_old_cluster_for_valid_slots().
  */
-static void
-get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
+static char *
+get_old_cluster_logical_slot_infos_query(void)
 {
-	PGconn	   *conn;
-	PGresult   *res;
-	LogicalSlotInfo *slotinfos = NULL;
-	int			num_slots;
-
-	/* Logical slots can be migrated since PG17. */
-	if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1600)
-		return;
-
-	conn = connectToServer(&old_cluster, dbinfo->db_name);
-
 	/*
 	 * Fetch the logical replication slot information. The check whether the
 	 * slot is considered caught up is done by an upgrade function. This
@@ -665,18 +666,32 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
-							"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
-							"FROM pg_catalog.pg_replication_slots "
-							"WHERE slot_type = 'logical' AND "
-							"database = current_database() AND "
-							"temporary IS FALSE;",
-							user_opts.live_check ? "FALSE" :
-							"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
-							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
-							"END)");
-
-	num_slots = PQntuples(res);
+	return psprintf("SELECT slot_name, plugin, two_phase, failover, "
+					"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
+					"FROM pg_catalog.pg_replication_slots "
+					"WHERE slot_type = 'logical' AND "
+					"database = current_database() AND "
+					"temporary IS FALSE;",
+					user_opts.live_check ? "FALSE" :
+					"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
+					"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
+					"END)");
+}
+
+/*
+ * Callback function for processing results of the query returned by
+ * get_old_cluster_logical_slot_infos_query(), which is used for
+ * get_db_rel_and_slot_infos()'s UpgradeTask.  This function stores the logical
+ * slot information for later use.
+ */
+static void
+process_old_cluster_logical_slot_infos(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	LogicalSlotInfo *slotinfos = NULL;
+	int			num_slots = PQntuples(res);
+
+	AssertVariableIsOfType(&process_old_cluster_logical_slot_infos,
+						   UpgradeTaskProcessCB);
 
 	if (num_slots)
 	{
@@ -709,9 +724,6 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 		}
 	}
 
-	PQclear(res);
-	PQfinish(conn);
-
 	dbinfo->slot_arr.slots = slotinfos;
 	dbinfo->slot_arr.nslots = num_slots;
 }
-- 
2.39.3 (Apple Git-146)

v11-0004-Use-pg_upgrade-s-new-parallel-framework-to-get-l.patchtext/plain; charset=us-asciiDownload
From 547090052b0783c81ad0d2c6c694ce40a387740c Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 13:32:33 -0500
Subject: [PATCH v11 04/11] Use pg_upgrade's new parallel framework to get
 loadable libraries.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/function.c | 71 ++++++++++++++++++++++-------------
 1 file changed, 45 insertions(+), 26 deletions(-)

diff --git a/src/bin/pg_upgrade/function.c b/src/bin/pg_upgrade/function.c
index 7e3abed098..0588347b49 100644
--- a/src/bin/pg_upgrade/function.c
+++ b/src/bin/pg_upgrade/function.c
@@ -42,6 +42,30 @@ library_name_compare(const void *p1, const void *p2)
 					  ((const LibraryInfo *) p2)->dbnum);
 }
 
+/*
+ * Private state for get_loadable_libraries()'s UpgradeTask.
+ */
+struct loadable_libraries_state
+{
+	PGresult  **ress;			/* results for each database */
+	int			totaltups;		/* number of tuples in all results */
+};
+
+/*
+ * Callback function for processing results of query for
+ * get_loadable_libraries()'s UpgradeTask.  This function stores the results
+ * for later use within get_loadable_libraries().
+ */
+static void
+process_loadable_libraries(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct loadable_libraries_state *state = (struct loadable_libraries_state *) arg;
+
+	AssertVariableIsOfType(&process_loadable_libraries, UpgradeTaskProcessCB);
+
+	state->ress[dbinfo - old_cluster.dbarr.dbs] = res;
+	state->totaltups += PQntuples(res);
+}
 
 /*
  * get_loadable_libraries()
@@ -54,47 +78,41 @@ library_name_compare(const void *p1, const void *p2)
 void
 get_loadable_libraries(void)
 {
-	PGresult  **ress;
 	int			totaltups;
 	int			dbnum;
 	int			n_libinfos;
+	UpgradeTask *task = upgrade_task_create();
+	struct loadable_libraries_state state;
+	char	   *query;
 
-	ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
-	totaltups = 0;
+	state.ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
+	state.totaltups = 0;
 
-	/* Fetch all library names, removing duplicates within each DB */
-	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
+	query = psprintf("SELECT DISTINCT probin "
+					 "FROM pg_catalog.pg_proc "
+					 "WHERE prolang = %u AND "
+					 "probin IS NOT NULL AND "
+					 "oid >= %u;",
+					 ClanguageId,
+					 FirstNormalObjectId);
 
-		/*
-		 * Fetch all libraries containing non-built-in C functions in this DB.
-		 */
-		ress[dbnum] = executeQueryOrDie(conn,
-										"SELECT DISTINCT probin "
-										"FROM pg_catalog.pg_proc "
-										"WHERE prolang = %u AND "
-										"probin IS NOT NULL AND "
-										"oid >= %u;",
-										ClanguageId,
-										FirstNormalObjectId);
-		totaltups += PQntuples(ress[dbnum]);
-
-		PQfinish(conn);
-	}
+	upgrade_task_add_step(task, query, process_loadable_libraries,
+						  false, &state);
+
+	upgrade_task_run(task, &old_cluster);
+	upgrade_task_free(task);
 
 	/*
 	 * Allocate memory for required libraries and logical replication output
 	 * plugins.
 	 */
-	n_libinfos = totaltups + count_old_cluster_logical_slots();
+	n_libinfos = state.totaltups + count_old_cluster_logical_slots();
 	os_info.libraries = (LibraryInfo *) pg_malloc(sizeof(LibraryInfo) * n_libinfos);
 	totaltups = 0;
 
 	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
 	{
-		PGresult   *res = ress[dbnum];
+		PGresult   *res = state.ress[dbnum];
 		int			ntups;
 		int			rowno;
 		LogicalSlotInfoArr *slot_arr = &old_cluster.dbarr.dbs[dbnum].slot_arr;
@@ -129,7 +147,8 @@ get_loadable_libraries(void)
 		}
 	}
 
-	pg_free(ress);
+	pg_free(state.ress);
+	pg_free(query);
 
 	os_info.num_libraries = totaltups;
 }
-- 
2.39.3 (Apple Git-146)

v11-0005-Use-pg_upgrade-s-new-parallel-framework-for-exte.patchtext/plain; charset=us-asciiDownload
From 7ea2132ea86eedaea916e4356d6448af3f769aab Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 14:18:39 -0500
Subject: [PATCH v11 05/11] Use pg_upgrade's new parallel framework for
 extension updates.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/version.c | 94 +++++++++++++++++++-----------------
 1 file changed, 49 insertions(+), 45 deletions(-)

diff --git a/src/bin/pg_upgrade/version.c b/src/bin/pg_upgrade/version.c
index 2de6dffccd..5084b08805 100644
--- a/src/bin/pg_upgrade/version.c
+++ b/src/bin/pg_upgrade/version.c
@@ -139,6 +139,41 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 		check_ok();
 }
 
+/*
+ * Callback function for processing results of query for
+ * report_extension_updates()'s UpgradeTask.  If the query returned any rows,
+ * write the details to the report file.
+ */
+static void
+process_extension_updates(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_name = PQfnumber(res, "name");
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+
+	AssertVariableIsOfType(&process_extension_updates, UpgradeTaskProcessCB);
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
+		{
+			PQExpBufferData connectbuf;
+
+			initPQExpBuffer(&connectbuf);
+			appendPsqlMetaConnect(&connectbuf, dbinfo->db_name);
+			fputs(connectbuf.data, report->file);
+			termPQExpBuffer(&connectbuf);
+			db_used = true;
+		}
+		fprintf(report->file, "ALTER EXTENSION %s UPDATE;\n",
+				quote_identifier(PQgetvalue(res, rowno, i_name)));
+	}
+}
+
 /*
  * report_extension_updates()
  *	Report extensions that should be updated.
@@ -146,57 +181,26 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 void
 report_extension_updates(ClusterInfo *cluster)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char	   *output_path = "update_extensions.sql";
+	UpgradeTaskReport report;
+	UpgradeTask *task = upgrade_task_create();
+	const char *query = "SELECT name "
+		"FROM pg_available_extensions "
+		"WHERE installed_version != default_version";
 
 	prep_status("Checking for extension updates");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_name;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* find extensions needing updates */
-		res = executeQueryOrDie(conn,
-								"SELECT name "
-								"FROM pg_available_extensions "
-								"WHERE installed_version != default_version"
-			);
+	report.file = NULL;
+	strcpy(report.path, "update_extensions.sql");
 
-		ntups = PQntuples(res);
-		i_name = PQfnumber(res, "name");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				PQExpBufferData connectbuf;
+	upgrade_task_add_step(task, query, process_extension_updates,
+						  true, &report);
 
-				initPQExpBuffer(&connectbuf);
-				appendPsqlMetaConnect(&connectbuf, active_db->db_name);
-				fputs(connectbuf.data, script);
-				termPQExpBuffer(&connectbuf);
-				db_used = true;
-			}
-			fprintf(script, "ALTER EXTENSION %s UPDATE;\n",
-					quote_identifier(PQgetvalue(res, rowno, i_name)));
-		}
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
 
-		PQclear(res);
-
-		PQfinish(conn);
-	}
-
-	if (script)
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		report_status(PG_REPORT, "notice");
 		pg_log(PG_REPORT, "\n"
 			   "Your installation contains extensions that should be updated\n"
@@ -204,7 +208,7 @@ report_extension_updates(ClusterInfo *cluster)
 			   "    %s\n"
 			   "when executed by psql by the database superuser will update\n"
 			   "these extensions.",
-			   output_path);
+			   report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v11-0006-Use-pg_upgrade-s-new-parallel-framework-for-data.patchtext/plain; charset=us-asciiDownload
From 3b0c42e31db602d76a400ce460b9d76ebc354991 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Sat, 6 Jul 2024 21:06:31 -0500
Subject: [PATCH v11 06/11] Use pg_upgrade's new parallel framework for data
 type checks.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 351 ++++++++++++++++++++-----------------
 1 file changed, 191 insertions(+), 160 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index f8160e0140..f935b53e1f 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -314,6 +314,147 @@ static DataTypesUsageChecks data_types_usage_checks[] =
 	}
 };
 
+/*
+ * Private state for check_for_data_types_usage()'s UpgradeTask.
+ */
+struct data_type_check_state
+{
+	DataTypesUsageChecks *check;	/* the check for this step */
+	bool	   *result;			/* true if check failed for any database */
+	PQExpBuffer *report;		/* buffer for report on failed checks */
+};
+
+/*
+ * Returns a palloc'd query string for the data type check, for use by
+ * check_for_data_types_usage()'s UpgradeTask.
+ */
+static char *
+data_type_check_query(int checknum)
+{
+	DataTypesUsageChecks *check = &data_types_usage_checks[checknum];
+
+	return psprintf("WITH RECURSIVE oids AS ( "
+	/* start with the type(s) returned by base_query */
+					"	%s "
+					"	UNION ALL "
+					"	SELECT * FROM ( "
+	/* inner WITH because we can only reference the CTE once */
+					"		WITH x AS (SELECT oid FROM oids) "
+	/* domains on any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
+					"			UNION ALL "
+	/* arrays over any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
+					"			UNION ALL "
+	/* composite types containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
+					"			WHERE t.typtype = 'c' AND "
+					"				  t.oid = c.reltype AND "
+					"				  c.oid = a.attrelid AND "
+					"				  NOT a.attisdropped AND "
+					"				  a.atttypid = x.oid "
+					"			UNION ALL "
+	/* ranges containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
+					"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
+					"	) foo "
+					") "
+	/* now look for stored columns of any such type */
+					"SELECT n.nspname, c.relname, a.attname "
+					"FROM	pg_catalog.pg_class c, "
+					"		pg_catalog.pg_namespace n, "
+					"		pg_catalog.pg_attribute a "
+					"WHERE	c.oid = a.attrelid AND "
+					"		NOT a.attisdropped AND "
+					"		a.atttypid IN (SELECT oid FROM oids) AND "
+					"		c.relkind IN ("
+					CppAsString2(RELKIND_RELATION) ", "
+					CppAsString2(RELKIND_MATVIEW) ", "
+					CppAsString2(RELKIND_INDEX) ") AND "
+					"		c.relnamespace = n.oid AND "
+	/* exclude possible orphaned temp tables */
+					"		n.nspname !~ '^pg_temp_' AND "
+					"		n.nspname !~ '^pg_toast_temp_' AND "
+	/* exclude system catalogs, too */
+					"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
+					check->base_query);
+}
+
+/*
+ * Callback function for processing results of queries for
+ * check_for_data_types_usage()'s UpgradeTask.  If the query returned any rows
+ * (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_data_type_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct data_type_check_state *state = (struct data_type_check_state *) arg;
+	int			ntups = PQntuples(res);
+
+	AssertVariableIsOfType(&process_data_type_check, UpgradeTaskProcessCB);
+
+	if (ntups)
+	{
+		char		output_path[MAXPGPATH];
+		int			i_nspname;
+		int			i_relname;
+		int			i_attname;
+		FILE	   *script = NULL;
+		bool		db_used = false;
+
+		snprintf(output_path, sizeof(output_path), "%s/%s",
+				 log_opts.basedir,
+				 state->check->report_filename);
+
+		/*
+		 * Make sure we have a buffer to save reports to now that we found a
+		 * first failing check.
+		 */
+		if (*state->report == NULL)
+			*state->report = createPQExpBuffer();
+
+		/*
+		 * If this is the first time we see an error for the check in question
+		 * then print a status message of the failure.
+		 */
+		if (!(*state->result))
+		{
+			pg_log(PG_REPORT, "    failed check: %s", _(state->check->status));
+			appendPQExpBuffer(*state->report, "\n%s\n%s    %s\n",
+							  _(state->check->report_text),
+							  _("A list of the problem columns is in the file:"),
+							  output_path);
+		}
+		*state->result = true;
+
+		i_nspname = PQfnumber(res, "nspname");
+		i_relname = PQfnumber(res, "relname");
+		i_attname = PQfnumber(res, "attname");
+
+		for (int rowno = 0; rowno < ntups; rowno++)
+		{
+			if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
+				pg_fatal("could not open file \"%s\": %m", output_path);
+
+			if (!db_used)
+			{
+				fprintf(script, "In database: %s\n", dbinfo->db_name);
+				db_used = true;
+			}
+			fprintf(script, "  %s.%s.%s\n",
+					PQgetvalue(res, rowno, i_nspname),
+					PQgetvalue(res, rowno, i_relname),
+					PQgetvalue(res, rowno, i_attname));
+		}
+
+		if (script)
+		{
+			fclose(script);
+			script = NULL;
+		}
+	}
+}
+
 /*
  * check_for_data_types_usage()
  *	Detect whether there are any stored columns depending on given type(s)
@@ -334,13 +475,15 @@ static DataTypesUsageChecks data_types_usage_checks[] =
  * there's no storage involved in a view.
  */
 static void
-check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
+check_for_data_types_usage(ClusterInfo *cluster)
 {
-	bool		found = false;
 	bool	   *results;
-	PQExpBufferData report;
-	DataTypesUsageChecks *tmp = checks;
+	PQExpBuffer report = NULL;
+	DataTypesUsageChecks *tmp = data_types_usage_checks;
 	int			n_data_types_usage_checks = 0;
+	UpgradeTask *task = upgrade_task_create();
+	char	  **queries = NULL;
+	struct data_type_check_state *states;
 
 	prep_status("Checking data type usage");
 
@@ -353,175 +496,63 @@ check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
 
 	/* Prepare an array to store the results of checks in */
 	results = pg_malloc0(sizeof(bool) * n_data_types_usage_checks);
+	queries = pg_malloc0(sizeof(char *) * n_data_types_usage_checks);
+	states = pg_malloc0(sizeof(struct data_type_check_state) * n_data_types_usage_checks);
 
-	/*
-	 * Connect to each database in the cluster and run all defined checks
-	 * against that database before trying the next one.
-	 */
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int i = 0; i < n_data_types_usage_checks; i++)
 	{
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
+		DataTypesUsageChecks *check = &data_types_usage_checks[i];
 
-		for (int checknum = 0; checknum < n_data_types_usage_checks; checknum++)
+		if (check->threshold_version == MANUAL_CHECK)
 		{
-			PGresult   *res;
-			int			ntups;
-			int			i_nspname;
-			int			i_relname;
-			int			i_attname;
-			FILE	   *script = NULL;
-			bool		db_used = false;
-			char		output_path[MAXPGPATH];
-			DataTypesUsageChecks *cur_check = &checks[checknum];
-
-			if (cur_check->threshold_version == MANUAL_CHECK)
-			{
-				Assert(cur_check->version_hook);
-
-				/*
-				 * Make sure that the check applies to the current cluster
-				 * version and skip if not. If no check hook has been defined
-				 * we run the check for all versions.
-				 */
-				if (!cur_check->version_hook(cluster))
-					continue;
-			}
-			else if (cur_check->threshold_version != ALL_VERSIONS)
-			{
-				if (GET_MAJOR_VERSION(cluster->major_version) > cur_check->threshold_version)
-					continue;
-			}
-			else
-				Assert(cur_check->threshold_version == ALL_VERSIONS);
-
-			snprintf(output_path, sizeof(output_path), "%s/%s",
-					 log_opts.basedir,
-					 cur_check->report_filename);
+			Assert(check->version_hook);
 
 			/*
-			 * The type(s) of interest might be wrapped in a domain, array,
-			 * composite, or range, and these container types can be nested
-			 * (to varying extents depending on server version, but that's not
-			 * of concern here).  To handle all these cases we need a
-			 * recursive CTE.
+			 * Make sure that the check applies to the current cluster version
+			 * and skip it if not.
 			 */
-			res = executeQueryOrDie(conn,
-									"WITH RECURSIVE oids AS ( "
-			/* start with the type(s) returned by base_query */
-									"	%s "
-									"	UNION ALL "
-									"	SELECT * FROM ( "
-			/* inner WITH because we can only reference the CTE once */
-									"		WITH x AS (SELECT oid FROM oids) "
-			/* domains on any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
-									"			UNION ALL "
-			/* arrays over any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
-									"			UNION ALL "
-			/* composite types containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
-									"			WHERE t.typtype = 'c' AND "
-									"				  t.oid = c.reltype AND "
-									"				  c.oid = a.attrelid AND "
-									"				  NOT a.attisdropped AND "
-									"				  a.atttypid = x.oid "
-									"			UNION ALL "
-			/* ranges containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
-									"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
-									"	) foo "
-									") "
-			/* now look for stored columns of any such type */
-									"SELECT n.nspname, c.relname, a.attname "
-									"FROM	pg_catalog.pg_class c, "
-									"		pg_catalog.pg_namespace n, "
-									"		pg_catalog.pg_attribute a "
-									"WHERE	c.oid = a.attrelid AND "
-									"		NOT a.attisdropped AND "
-									"		a.atttypid IN (SELECT oid FROM oids) AND "
-									"		c.relkind IN ("
-									CppAsString2(RELKIND_RELATION) ", "
-									CppAsString2(RELKIND_MATVIEW) ", "
-									CppAsString2(RELKIND_INDEX) ") AND "
-									"		c.relnamespace = n.oid AND "
-			/* exclude possible orphaned temp tables */
-									"		n.nspname !~ '^pg_temp_' AND "
-									"		n.nspname !~ '^pg_toast_temp_' AND "
-			/* exclude system catalogs, too */
-									"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
-									cur_check->base_query);
-
-			ntups = PQntuples(res);
+			if (!check->version_hook(cluster))
+				continue;
+		}
+		else if (check->threshold_version != ALL_VERSIONS)
+		{
+			if (GET_MAJOR_VERSION(cluster->major_version) > check->threshold_version)
+				continue;
+		}
+		else
+			Assert(check->threshold_version == ALL_VERSIONS);
 
-			/*
-			 * The datatype was found, so extract the data and log to the
-			 * requested filename. We need to open the file for appending
-			 * since the check might have already found the type in another
-			 * database earlier in the loop.
-			 */
-			if (ntups)
-			{
-				/*
-				 * Make sure we have a buffer to save reports to now that we
-				 * found a first failing check.
-				 */
-				if (!found)
-					initPQExpBuffer(&report);
-				found = true;
-
-				/*
-				 * If this is the first time we see an error for the check in
-				 * question then print a status message of the failure.
-				 */
-				if (!results[checknum])
-				{
-					pg_log(PG_REPORT, "    failed check: %s", _(cur_check->status));
-					appendPQExpBuffer(&report, "\n%s\n%s    %s\n",
-									  _(cur_check->report_text),
-									  _("A list of the problem columns is in the file:"),
-									  output_path);
-				}
-				results[checknum] = true;
-
-				i_nspname = PQfnumber(res, "nspname");
-				i_relname = PQfnumber(res, "relname");
-				i_attname = PQfnumber(res, "attname");
-
-				for (int rowno = 0; rowno < ntups; rowno++)
-				{
-					if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
-						pg_fatal("could not open file \"%s\": %m", output_path);
-
-					if (!db_used)
-					{
-						fprintf(script, "In database: %s\n", active_db->db_name);
-						db_used = true;
-					}
-					fprintf(script, "  %s.%s.%s\n",
-							PQgetvalue(res, rowno, i_nspname),
-							PQgetvalue(res, rowno, i_relname),
-							PQgetvalue(res, rowno, i_attname));
-				}
-
-				if (script)
-				{
-					fclose(script);
-					script = NULL;
-				}
-			}
+		queries[i] = data_type_check_query(i);
 
-			PQclear(res);
-		}
+		states[i].check = check;
+		states[i].result = &results[i];
+		states[i].report = &report;
 
-		PQfinish(conn);
+		upgrade_task_add_step(task, queries[i], process_data_type_check,
+							  true, &states[i]);
 	}
 
-	if (found)
-		pg_fatal("Data type checks failed: %s", report.data);
+	/*
+	 * Connect to each database in the cluster and run all defined checks
+	 * against that database before trying the next one.
+	 */
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
+
+	if (report)
+	{
+		pg_fatal("Data type checks failed: %s", report->data);
+		destroyPQExpBuffer(report);
+	}
 
 	pg_free(results);
+	for (int i = 0; i < n_data_types_usage_checks; i++)
+	{
+		if (queries[i])
+			pg_free(queries[i]);
+	}
+	pg_free(queries);
+	pg_free(states);
 
 	check_ok();
 }
@@ -616,7 +647,7 @@ check_and_dump_old_cluster(void)
 		check_old_cluster_subscription_state();
 	}
 
-	check_for_data_types_usage(&old_cluster, data_types_usage_checks);
+	check_for_data_types_usage(&old_cluster);
 
 	/*
 	 * PG 14 changed the function signature of encoding conversion functions.
-- 
2.39.3 (Apple Git-146)

v11-0007-Use-pg_upgrade-s-new-parallel-framework-for-isn-.patchtext/plain; charset=us-asciiDownload
From a3835a8624c380cd822b73d5b6d74860ec1fcd29 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:00:20 -0500
Subject: [PATCH v11 07/11] Use pg_upgrade's new parallel framework for isn and
 int8 check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 97 ++++++++++++++++++++------------------
 1 file changed, 50 insertions(+), 47 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index f935b53e1f..b8af7e541b 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1225,6 +1225,39 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 	check_ok();
 }
 
+/*
+ * Callback function for processing result of query for
+ * check_for_isn_and_int8_passing_mismatch()'s UpgradeTask.  If the query
+ * returned any rows (i.e., the check failed), write the details to the report
+ * file.
+ */
+static void
+process_isn_and_int8_passing_mismatch(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_proname = PQfnumber(res, "proname");
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+
+	AssertVariableIsOfType(&process_isn_and_int8_passing_mismatch,
+						   UpgradeTaskProcessCB);
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
+		{
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(report->file, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_proname));
+	}
+}
 
 /*
  *	check_for_isn_and_int8_passing_mismatch()
@@ -1236,9 +1269,13 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 static void
 check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTask *task;
+	UpgradeTaskReport report;
+	const char *query = "SELECT n.nspname, p.proname "
+		"FROM   pg_catalog.pg_proc p, "
+		"       pg_catalog.pg_namespace n "
+		"WHERE  p.pronamespace = n.oid AND "
+		"       p.probin = '$libdir/isn'";
 
 	prep_status("Checking for contrib/isn with bigint-passing mismatch");
 
@@ -1250,54 +1287,20 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 		return;
 	}
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
 			 log_opts.basedir,
 			 "contrib_isn_and_int8_pass_by_value.txt");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_proname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* Find any functions coming from contrib/isn */
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, p.proname "
-								"FROM	pg_catalog.pg_proc p, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	p.pronamespace = n.oid AND "
-								"		p.probin = '$libdir/isn'");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_proname = PQfnumber(res, "proname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_proname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	task = upgrade_task_create();
+	upgrade_task_add_step(task, query, process_isn_and_int8_passing_mismatch,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
 
-	if (script)
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains \"contrib/isn\" functions which rely on the\n"
 				 "bigint data type.  Your old and new clusters pass bigint values\n"
@@ -1305,7 +1308,7 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 				 "manually dump databases in the old cluster that use \"contrib/isn\"\n"
 				 "facilities, drop them, perform the upgrade, and then restore them.  A\n"
 				 "list of the problem functions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v11-0008-Use-pg_upgrade-s-new-parallel-framework-for-post.patchtext/plain; charset=us-asciiDownload
From a1803394e21534156306144deeef1065520981b9 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 15:10:19 -0500
Subject: [PATCH v11 08/11] Use pg_upgrade's new parallel framework for postfix
 operator check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 146 +++++++++++++++++++------------------
 1 file changed, 75 insertions(+), 71 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index b8af7e541b..28c4ddbca3 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1315,95 +1315,99 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 }
 
 /*
- * Verify that no user defined postfix operators exist.
+ * Callback function for processing result of query for
+ * check_for_user_defined_postfix_ops()'s UpgradeTask.  If the query returned
+ * any rows (i.e., the check failed), write the details to the report file.
  */
 static void
-check_for_user_defined_postfix_ops(ClusterInfo *cluster)
+process_user_defined_postfix_ops(DbInfo *dbinfo, PGresult *res, void *arg)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	int			ntups = PQntuples(res);
+	bool		db_used = false;
+	int			i_oproid = PQfnumber(res, "oproid");
+	int			i_oprnsp = PQfnumber(res, "oprnsp");
+	int			i_oprname = PQfnumber(res, "oprname");
+	int			i_typnsp = PQfnumber(res, "typnsp");
+	int			i_typname = PQfnumber(res, "typname");
 
-	prep_status("Checking for user-defined postfix operators");
+	AssertVariableIsOfType(&process_user_defined_postfix_ops,
+						   UpgradeTaskProcessCB);
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
-			 log_opts.basedir,
-			 "postfix_ops.txt");
+	if (!ntups)
+		return;
 
-	/* Find any user defined postfix operators */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int rowno = 0; rowno < ntups; rowno++)
 	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_oproid,
-					i_oprnsp,
-					i_oprname,
-					i_typnsp,
-					i_typname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT o.oid AS oproid, "
-								"       n.nspname AS oprnsp, "
-								"       o.oprname, "
-								"       tn.nspname AS typnsp, "
-								"       t.typname "
-								"FROM pg_catalog.pg_operator o, "
-								"     pg_catalog.pg_namespace n, "
-								"     pg_catalog.pg_type t, "
-								"     pg_catalog.pg_namespace tn "
-								"WHERE o.oprnamespace = n.oid AND "
-								"      o.oprleft = t.oid AND "
-								"      t.typnamespace = tn.oid AND "
-								"      o.oprright = 0 AND "
-								"      o.oid >= 16384");
-		ntups = PQntuples(res);
-		i_oproid = PQfnumber(res, "oproid");
-		i_oprnsp = PQfnumber(res, "oprnsp");
-		i_oprname = PQfnumber(res, "oprname");
-		i_typnsp = PQfnumber(res, "typnsp");
-		i_typname = PQfnumber(res, "typname");
-		for (rowno = 0; rowno < ntups; rowno++)
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
 		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
-					PQgetvalue(res, rowno, i_oproid),
-					PQgetvalue(res, rowno, i_oprnsp),
-					PQgetvalue(res, rowno, i_oprname),
-					PQgetvalue(res, rowno, i_typnsp),
-					PQgetvalue(res, rowno, i_typname));
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
 		}
+		fprintf(report->file, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
+				PQgetvalue(res, rowno, i_oproid),
+				PQgetvalue(res, rowno, i_oprnsp),
+				PQgetvalue(res, rowno, i_oprname),
+				PQgetvalue(res, rowno, i_typnsp),
+				PQgetvalue(res, rowno, i_typname));
+	}
+}
 
-		PQclear(res);
+/*
+ * Verify that no user defined postfix operators exist.
+ */
+static void
+check_for_user_defined_postfix_ops(ClusterInfo *cluster)
+{
+	UpgradeTaskReport report;
+	UpgradeTask *task = upgrade_task_create();
+	const char *query;
 
-		PQfinish(conn);
-	}
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	query = "SELECT o.oid AS oproid, "
+		"       n.nspname AS oprnsp, "
+		"       o.oprname, "
+		"       tn.nspname AS typnsp, "
+		"       t.typname "
+		"FROM pg_catalog.pg_operator o, "
+		"     pg_catalog.pg_namespace n, "
+		"     pg_catalog.pg_type t, "
+		"     pg_catalog.pg_namespace tn "
+		"WHERE o.oprnamespace = n.oid AND "
+		"      o.oprleft = t.oid AND "
+		"      t.typnamespace = tn.oid AND "
+		"      o.oprright = 0 AND "
+		"      o.oid >= 16384";
 
-	if (script)
+	prep_status("Checking for user-defined postfix operators");
+
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
+			 log_opts.basedir,
+			 "postfix_ops.txt");
+
+	upgrade_task_add_step(task, query, process_user_defined_postfix_ops,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
+
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains user-defined postfix operators, which are not\n"
 				 "supported anymore.  Consider dropping the postfix operators and replacing\n"
 				 "them with prefix operators or function calls.\n"
 				 "A list of user-defined postfix operators is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v11-0009-Use-pg_upgrade-s-new-parallel-framework-for-poly.patchtext/plain; charset=us-asciiDownload
From 28f8eefff5c173d6a79c49e9fa9754a4603cfca2 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 15:21:29 -0500
Subject: [PATCH v11 09/11] Use pg_upgrade's new parallel framework for
 polymorphics check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 159 +++++++++++++++++++------------------
 1 file changed, 83 insertions(+), 76 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 28c4ddbca3..92a3aa6a77 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1413,6 +1413,40 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 		check_ok();
 }
 
+/*
+ * Callback function for processing results of query for
+ * check_for_incompatible_polymorphics()'s UpgradeTask.  If the query returned
+ * any rows (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_incompat_polymorphics(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_objkind = PQfnumber(res, "objkind");
+	int			i_objname = PQfnumber(res, "objname");
+
+	AssertVariableIsOfType(&process_incompat_polymorphics,
+						   UpgradeTaskProcessCB);
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
+		{
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+
+		fprintf(report->file, "  %s: %s\n",
+				PQgetvalue(res, rowno, i_objkind),
+				PQgetvalue(res, rowno, i_objname));
+	}
+}
+
 /*
  *	check_for_incompatible_polymorphics()
  *
@@ -1422,14 +1456,15 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 static void
 check_for_incompatible_polymorphics(ClusterInfo *cluster)
 {
-	PGresult   *res;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
 	PQExpBufferData old_polymorphics;
+	UpgradeTask *task = upgrade_task_create();
+	UpgradeTaskReport report;
+	char	   *query;
 
 	prep_status("Checking for incompatible polymorphic functions");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
 			 log_opts.basedir,
 			 "incompatible_polymorphics.txt");
 
@@ -1453,80 +1488,51 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 							 ", 'array_positions(anyarray,anyelement)'"
 							 ", 'width_bucket(anyelement,anyarray)'");
 
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		bool		db_used = false;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-		int			ntups;
-		int			i_objkind,
-					i_objname;
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-		/* Aggregate transition functions */
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Aggregate final functions */
-								"UNION ALL "
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Operators */
-								"UNION ALL "
-								"SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
-								"FROM pg_operator AS op "
-								"WHERE op.oid >= 16384 "
-								"AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
-								old_polymorphics.data,
-								old_polymorphics.data,
-								old_polymorphics.data);
-
-		ntups = PQntuples(res);
-
-		i_objkind = PQfnumber(res, "objkind");
-		i_objname = PQfnumber(res, "objname");
-
-		for (int rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-
-			fprintf(script, "  %s: %s\n",
-					PQgetvalue(res, rowno, i_objkind),
-					PQgetvalue(res, rowno, i_objname));
-		}
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
 
-		PQclear(res);
-		PQfinish(conn);
-	}
+	/* Aggregate transition functions */
+	query = psprintf("SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					 "FROM pg_proc AS p "
+					 "JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					 "JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
+					 "WHERE p.oid >= 16384 "
+					 "AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Aggregate final functions */
+					 "UNION ALL "
+					 "SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					 "FROM pg_proc AS p "
+					 "JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					 "JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
+					 "WHERE p.oid >= 16384 "
+					 "AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Operators */
+					 "UNION ALL "
+					 "SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
+					 "FROM pg_operator AS op "
+					 "WHERE op.oid >= 16384 "
+					 "AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
+					 old_polymorphics.data,
+					 old_polymorphics.data,
+					 old_polymorphics.data);
+
+	upgrade_task_add_step(task, query, process_incompat_polymorphics,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
 
-	if (script)
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains user-defined objects that refer to internal\n"
 				 "polymorphic functions with arguments of type \"anyarray\" or \"anyelement\".\n"
@@ -1534,12 +1540,13 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 				 "afterwards, changing them to refer to the new corresponding functions with\n"
 				 "arguments of type \"anycompatiblearray\" and \"anycompatible\".\n"
 				 "A list of the problematic objects is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
 
 	termPQExpBuffer(&old_polymorphics);
+	pg_free(query);
 }
 
 /*
-- 
2.39.3 (Apple Git-146)

v11-0010-Use-pg_upgrade-s-new-parallel-framework-for-WITH.patchtext/plain; charset=us-asciiDownload
From 96f83a360944941cbf2963a7795e28ad4f3537c1 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 15:27:37 -0500
Subject: [PATCH v11 10/11] Use pg_upgrade's new parallel framework for WITH
 OIDS check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 100 +++++++++++++++++++------------------
 1 file changed, 52 insertions(+), 48 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 92a3aa6a77..dff440b29a 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1550,72 +1550,76 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 }
 
 /*
- * Verify that no tables are declared WITH OIDS.
+ * Callback function for processing results of query for
+ * check_for_tables_with_oids()'s UpgradeTask.  If the query returned any rows
+ * (i.e., the check failed), write the details to the report file.
  */
 static void
-check_for_tables_with_oids(ClusterInfo *cluster)
+process_with_oids_check(DbInfo *dbinfo, PGresult *res, void *arg)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
 
-	prep_status("Checking for tables WITH OIDS");
+	AssertVariableIsOfType(&process_with_oids_check, UpgradeTaskProcessCB);
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
-			 log_opts.basedir,
-			 "tables_with_oids.txt");
+	if (!ntups)
+		return;
 
-	/* Find any tables declared WITH OIDS */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int rowno = 0; rowno < ntups; rowno++)
 	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_relname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, c.relname "
-								"FROM	pg_catalog.pg_class c, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	c.relnamespace = n.oid AND "
-								"		c.relhasoids AND"
-								"       n.nspname NOT IN ('pg_catalog')");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_relname = PQfnumber(res, "relname");
-		for (rowno = 0; rowno < ntups; rowno++)
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
 		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_relname));
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
 		}
+		fprintf(report->file, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_relname));
+	}
+}
 
-		PQclear(res);
+/*
+ * Verify that no tables are declared WITH OIDS.
+ */
+static void
+check_for_tables_with_oids(ClusterInfo *cluster)
+{
+	UpgradeTaskReport report;
+	UpgradeTask *task = upgrade_task_create();
+	const char *query = "SELECT n.nspname, c.relname "
+		"FROM   pg_catalog.pg_class c, "
+		"       pg_catalog.pg_namespace n "
+		"WHERE  c.relnamespace = n.oid AND "
+		"       c.relhasoids AND"
+		"       n.nspname NOT IN ('pg_catalog')";
 
-		PQfinish(conn);
-	}
+	prep_status("Checking for tables WITH OIDS");
 
-	if (script)
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
+			 log_opts.basedir,
+			 "tables_with_oids.txt");
+
+	upgrade_task_add_step(task, query, process_with_oids_check,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
+
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains tables declared WITH OIDS, which is not\n"
 				 "supported anymore.  Consider removing the oid column using\n"
 				 "    ALTER TABLE ... SET WITHOUT OIDS;\n"
 				 "A list of tables with the problem is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v11-0011-Use-pg_upgrade-s-parallel-framework-for-encoding.patchtext/plain; charset=us-asciiDownload
From 5fa06c396ec894ba5625ef00cbc621798547c2a7 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 15:35:31 -0500
Subject: [PATCH v11 11/11] Use pg_upgrade's parallel framework for encoding
 conversion check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 120 ++++++++++++++++++++-----------------
 1 file changed, 64 insertions(+), 56 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index dff440b29a..01ab3d0694 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1684,81 +1684,89 @@ check_for_pg_role_prefix(ClusterInfo *cluster)
 }
 
 /*
- * Verify that no user-defined encoding conversions exist.
+ * Callback function for processing results of query for
+ * check_for_user_defined_encoding_conversions()'s UpgradeTask.  If the query
+ * returned any rows (i.e., the check failed), write the details to the report
+ * file.
  */
 static void
-check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
+process_user_defined_encoding_conversions(DbInfo *dbinfo, PGresult *res, void *arg)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_conoid = PQfnumber(res, "conoid");
+	int			i_conname = PQfnumber(res, "conname");
+	int			i_nspname = PQfnumber(res, "nspname");
 
-	prep_status("Checking for user-defined encoding conversions");
+	AssertVariableIsOfType(&process_user_defined_encoding_conversions,
+						   UpgradeTaskProcessCB);
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
-			 log_opts.basedir,
-			 "encoding_conversions.txt");
+	if (!ntups)
+		return;
 
-	/* Find any user defined encoding conversions */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int rowno = 0; rowno < ntups; rowno++)
 	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_conoid,
-					i_conname,
-					i_nspname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT c.oid as conoid, c.conname, n.nspname "
-								"FROM pg_catalog.pg_conversion c, "
-								"     pg_catalog.pg_namespace n "
-								"WHERE c.connamespace = n.oid AND "
-								"      c.oid >= 16384");
-		ntups = PQntuples(res);
-		i_conoid = PQfnumber(res, "conoid");
-		i_conname = PQfnumber(res, "conname");
-		i_nspname = PQfnumber(res, "nspname");
-		for (rowno = 0; rowno < ntups; rowno++)
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
 		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s\n",
-					PQgetvalue(res, rowno, i_conoid),
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_conname));
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
 		}
+		fprintf(report->file, "  (oid=%s) %s.%s\n",
+				PQgetvalue(res, rowno, i_conoid),
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_conname));
+	}
+}
 
-		PQclear(res);
+/*
+ * Verify that no user-defined encoding conversions exist.
+ */
+static void
+check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
+{
+	UpgradeTaskReport report;
+	UpgradeTask *task = upgrade_task_create();
+	const char *query;
 
-		PQfinish(conn);
-	}
+	prep_status("Checking for user-defined encoding conversions");
 
-	if (script)
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
+			 log_opts.basedir,
+			 "encoding_conversions.txt");
+
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	query = "SELECT c.oid as conoid, c.conname, n.nspname "
+		"FROM pg_catalog.pg_conversion c, "
+		"     pg_catalog.pg_namespace n "
+		"WHERE c.connamespace = n.oid AND "
+		"      c.oid >= 16384";
+
+	upgrade_task_add_step(task, query,
+						  process_user_defined_encoding_conversions,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
+
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains user-defined encoding conversions.\n"
 				 "The conversion function parameters changed in PostgreSQL version 14\n"
 				 "so this cluster cannot currently be upgraded.  You can remove the\n"
 				 "encoding conversions in the old cluster and restart the upgrade.\n"
 				 "A list of user-defined encoding conversions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

#31Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#30)
11 attachment(s)
Re: optimizing pg_upgrade's once-in-each-database steps

In v12, I've moved the "queries" PQExpBuffer up to the UpgradeTask struct
so that we don't need to rebuild it for every database. I think this patch
set is in reasonable state, and I still plan to commit it this month.

--
nathan

Attachments:

v12-0001-Introduce-framework-for-parallelizing-various-pg.patchtext/plain; charset=us-asciiDownload
From 7b3a26bb8e418bdf1920f5fe9fe97afd2939d33a Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 10:45:59 -0500
Subject: [PATCH v12 01/11] Introduce framework for parallelizing various
 pg_upgrade tasks.

A number of pg_upgrade steps require connecting to every database
in the cluster and running the same query in each one.  When there
are many databases, these steps are particularly time-consuming,
especially since these steps are performed sequentially in a single
process.

This commit introduces a new framework that makes it easy to
parallelize most of these once-in-each-database tasks.
Specifically, it manages a simple state machine of slots and uses
libpq's asynchronous APIs to establish the connections and run the
queries.  The --jobs option is used to determine the number of
slots to use.  To use this new task framework, callers simply need
to provide the query and a callback function to process its
results, and the framework takes care of the rest.  A more complete
description is provided at the top of the new task.c file.

None of the eligible once-in-each-database tasks are converted to
use this new framework in this commit.  That will be done via
several follow-up commits.

Reviewed-by: Jeff Davis, Robert Haas, Daniel Gustafsson, Ilya Gladyshev, Corey Huinker
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 doc/src/sgml/ref/pgupgrade.sgml  |   6 +-
 src/bin/pg_upgrade/Makefile      |   1 +
 src/bin/pg_upgrade/meson.build   |   1 +
 src/bin/pg_upgrade/pg_upgrade.h  |  21 ++
 src/bin/pg_upgrade/task.c        | 483 +++++++++++++++++++++++++++++++
 src/tools/pgindent/typedefs.list |   5 +
 6 files changed, 514 insertions(+), 3 deletions(-)
 create mode 100644 src/bin/pg_upgrade/task.c

diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml
index 9877f2f01c..fc2d0ff845 100644
--- a/doc/src/sgml/ref/pgupgrade.sgml
+++ b/doc/src/sgml/ref/pgupgrade.sgml
@@ -118,7 +118,7 @@ PostgreSQL documentation
      <varlistentry>
       <term><option>-j <replaceable class="parameter">njobs</replaceable></option></term>
       <term><option>--jobs=<replaceable class="parameter">njobs</replaceable></option></term>
-      <listitem><para>number of simultaneous processes or threads to use
+      <listitem><para>number of simultaneous connections and processes/threads to use
       </para></listitem>
      </varlistentry>
 
@@ -587,8 +587,8 @@ NET STOP postgresql-&majorversion;
 
     <para>
      The <option>--jobs</option> option allows multiple CPU cores to be used
-     for copying/linking of files and to dump and restore database schemas
-     in parallel;  a good place to start is the maximum of the number of
+     for copying/linking of files, dumping and restoring database schemas
+     in parallel, etc.;  a good place to start is the maximum of the number of
      CPU cores and tablespaces.  This option can dramatically reduce the
      time to upgrade a multi-database server running on a multiprocessor
      machine.
diff --git a/src/bin/pg_upgrade/Makefile b/src/bin/pg_upgrade/Makefile
index bde91e2beb..f83d2b5d30 100644
--- a/src/bin/pg_upgrade/Makefile
+++ b/src/bin/pg_upgrade/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	relfilenumber.o \
 	server.o \
 	tablespace.o \
+	task.o \
 	util.o \
 	version.o
 
diff --git a/src/bin/pg_upgrade/meson.build b/src/bin/pg_upgrade/meson.build
index 9825fa3305..3d88419674 100644
--- a/src/bin/pg_upgrade/meson.build
+++ b/src/bin/pg_upgrade/meson.build
@@ -14,6 +14,7 @@ pg_upgrade_sources = files(
   'relfilenumber.c',
   'server.c',
   'tablespace.c',
+  'task.c',
   'util.c',
   'version.c',
 )
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index cdb6e2b759..53f693c2d4 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -494,3 +494,24 @@ void		parallel_transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr
 										  char *old_pgdata, char *new_pgdata,
 										  char *old_tablespace);
 bool		reap_child(bool wait_for_child);
+
+/* task.c */
+
+typedef void (*UpgradeTaskProcessCB) (DbInfo *dbinfo, PGresult *res, void *arg);
+
+/* struct definition is private to task.c */
+typedef struct UpgradeTask UpgradeTask;
+
+UpgradeTask *upgrade_task_create(void);
+void		upgrade_task_add_step(UpgradeTask *task, const char *query,
+								  UpgradeTaskProcessCB process_cb, bool free_result,
+								  void *arg);
+void		upgrade_task_run(const UpgradeTask *task, const ClusterInfo *cluster);
+void		upgrade_task_free(UpgradeTask *task);
+
+/* convenient type for common private data needed by several tasks */
+typedef struct
+{
+	FILE	   *file;
+	char		path[MAXPGPATH];
+} UpgradeTaskReport;
diff --git a/src/bin/pg_upgrade/task.c b/src/bin/pg_upgrade/task.c
new file mode 100644
index 0000000000..3618dc08ff
--- /dev/null
+++ b/src/bin/pg_upgrade/task.c
@@ -0,0 +1,483 @@
+/*
+ * task.c
+ *		framework for parallelizing pg_upgrade's once-in-each-database tasks
+ *
+ * This framework provides an efficient way of running the various
+ * once-in-each-database tasks required by pg_upgrade.  Specifically, it
+ * parallelizes these tasks by managing a simple state machine of
+ * user_opts.jobs slots and using libpq's asynchronous APIs to establish the
+ * connections and run the queries.  Callers simply need to create a callback
+ * function and build/execute an UpgradeTask.  A simple example follows:
+ *
+ *		static void
+ *		my_process_cb(DbInfo *dbinfo, PGresult *res, void *arg)
+ *		{
+ *			for (int i = 0; i < PQntuples(res); i++)
+ *			{
+ *				... process results ...
+ *			}
+ *		}
+ *
+ *		void
+ *		my_task(ClusterInfo *cluster)
+ *		{
+ *			UpgradeTask *task = upgrade_task_create();
+ *
+ *			upgrade_task_add_step(task,
+ *								  "... query text ...",
+ *								  my_process_cb,
+ *								  true,		// let the task free the PGresult
+ *								  NULL);	// "arg" pointer for callback
+ *			upgrade_task_run(task, cluster);
+ *			upgrade_task_free(task);
+ *		}
+ *
+ * Note that multiple steps can be added to a given task.  When there are
+ * multiple steps, the task will run all of the steps consecutively in the same
+ * database connection before freeing the connection and moving on.  In other
+ * words, it only ever initiates one connection to each database in the
+ * cluster for a given run.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ * src/bin/pg_upgrade/task.c
+ */
+
+#include "postgres_fe.h"
+
+#include "common/connect.h"
+#include "fe_utils/string_utils.h"
+#include "pg_upgrade.h"
+
+/*
+ * dbs_complete stores the number of databases that we have completed
+ * processing.  When this value equals the number of databases in the cluster,
+ * the task is finished.
+ */
+static int	dbs_complete;
+
+/*
+ * dbs_processing stores the index of the next database in the cluster's array
+ * of databases that will be picked up for processing.  It will always be
+ * greater than or equal to dbs_complete.
+ */
+static int	dbs_processing;
+
+/*
+ * This struct stores the information for a single step of a task.  Note that
+ * the query string is stored in the "queries" PQExpBuffer for the UpgradeTask.
+ * All steps in a task are run in a single connection before moving on to the
+ * next database (which requires a new connection).
+ */
+typedef struct UpgradeTaskStep
+{
+	UpgradeTaskProcessCB process_cb;	/* processes the results of the query */
+	bool		free_result;	/* should we free the result? */
+	void	   *arg;			/* pointer passed to process_cb */
+} UpgradeTaskStep;
+
+/*
+ * This struct is a thin wrapper around an array of steps, i.e.,
+ * UpgradeTaskStep, plus a PQExpBuffer for all the query strings.
+ */
+typedef struct UpgradeTask
+{
+	UpgradeTaskStep *steps;
+	int			num_steps;
+	PQExpBuffer queries;
+} UpgradeTask;
+
+/*
+ * The different states for a parallel slot.
+ */
+typedef enum
+{
+	FREE,						/* slot available for use in a new database */
+	CONNECTING,					/* waiting for connection to be established */
+	RUNNING_QUERIES,			/* running/processing queries in the task */
+} UpgradeTaskSlotState;
+
+/*
+ * We maintain an array of user_opts.jobs slots to execute the task.
+ */
+typedef struct
+{
+	UpgradeTaskSlotState state; /* state of the slot */
+	int			db_idx;			/* index of the database assigned to slot */
+	int			step_idx;		/* index of the current step of task */
+	PGconn	   *conn;			/* current connection managed by slot */
+	bool		ready;			/* slot is ready for processing */
+	bool		select_mode;	/* select() mode: true->read, false->write */
+	int			sock;			/* file descriptor for connection's socket */
+} UpgradeTaskSlot;
+
+/*
+ * Initializes an UpgradeTask.
+ */
+UpgradeTask *
+upgrade_task_create(void)
+{
+	UpgradeTask *task = pg_malloc0(sizeof(UpgradeTask));
+
+	task->queries = createPQExpBuffer();
+
+	/* All tasks must first set a secure search_path. */
+	upgrade_task_add_step(task, ALWAYS_SECURE_SEARCH_PATH_SQL, NULL, true, NULL);
+
+	return task;
+}
+
+/*
+ * Frees all storage associated with an UpgradeTask.
+ */
+void
+upgrade_task_free(UpgradeTask *task)
+{
+	if (task->steps)
+		pg_free(task->steps);
+
+	destroyPQExpBuffer(task->queries);
+
+	pg_free(task);
+}
+
+/*
+ * Adds a step to an UpgradeTask.  The steps will be executed in each database
+ * in the order in which they are added.
+ *
+ *	task: task object that must have been initialized via upgrade_task_create()
+ *	query: the query text
+ *	process_cb: function that processes the results of the query
+ *	free_result: should we free the PGresult, or leave it to the caller?
+ *	arg: pointer to task-specific data that is passed to each callback
+ */
+void
+upgrade_task_add_step(UpgradeTask *task, const char *query,
+					  UpgradeTaskProcessCB process_cb, bool free_result,
+					  void *arg)
+{
+	UpgradeTaskStep *new_step;
+
+	task->steps = pg_realloc(task->steps,
+							 ++task->num_steps * sizeof(UpgradeTaskStep));
+
+	new_step = &task->steps[task->num_steps - 1];
+	new_step->process_cb = process_cb;
+	new_step->free_result = free_result;
+	new_step->arg = arg;
+
+	appendPQExpBuffer(task->queries, "%s;", query);
+}
+
+/*
+ * Build a connection string for the slot's current database and asynchronously
+ * start a new connection, but do not wait for the connection to be
+ * established.
+ */
+static void
+start_conn(const ClusterInfo *cluster, UpgradeTaskSlot *slot)
+{
+	PQExpBufferData conn_opts;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db_idx];
+
+	/* Build connection string with proper quoting */
+	initPQExpBuffer(&conn_opts);
+	appendPQExpBufferStr(&conn_opts, "dbname=");
+	appendConnStrVal(&conn_opts, dbinfo->db_name);
+	appendPQExpBufferStr(&conn_opts, " user=");
+	appendConnStrVal(&conn_opts, os_info.user);
+	appendPQExpBuffer(&conn_opts, " port=%d", cluster->port);
+	if (cluster->sockdir)
+	{
+		appendPQExpBufferStr(&conn_opts, " host=");
+		appendConnStrVal(&conn_opts, cluster->sockdir);
+	}
+
+	slot->conn = PQconnectStart(conn_opts.data);
+
+	if (!slot->conn)
+		pg_fatal("failed to create connection with connection string: \"%s\"",
+				 conn_opts.data);
+
+	termPQExpBuffer(&conn_opts);
+}
+
+/*
+ * Run the process_cb callback function to process the result of a query, and
+ * free the result if the caller indicated we should do so.
+ */
+static void
+process_query_result(const ClusterInfo *cluster, UpgradeTaskSlot *slot,
+					 const UpgradeTask *task)
+{
+	UpgradeTaskStep *steps = &task->steps[slot->step_idx];
+	UpgradeTaskProcessCB process_cb = steps->process_cb;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db_idx];
+	PGresult   *res = PQgetResult(slot->conn);
+
+	if (PQstatus(slot->conn) == CONNECTION_BAD ||
+		(PQresultStatus(res) != PGRES_TUPLES_OK &&
+		 PQresultStatus(res) != PGRES_COMMAND_OK))
+		pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+
+	/*
+	 * We assume that a NULL process_cb callback function means there's
+	 * nothing to process.  This is primarily intended for the inital step in
+	 * every task that sets a safe search_path.
+	 */
+	if (process_cb)
+		(*process_cb) (dbinfo, res, steps->arg);
+
+	if (steps->free_result)
+		PQclear(res);
+}
+
+/*
+ * Advances the state machine for a given slot as necessary.
+ */
+static void
+process_slot(const ClusterInfo *cluster, UpgradeTaskSlot *slot, const UpgradeTask *task)
+{
+	if (!slot->ready)
+		return;
+
+	switch (slot->state)
+	{
+		case FREE:
+
+			/*
+			 * If all of the databases in the cluster have been processed or
+			 * are currently being processed by other slots, we are done.
+			 */
+			if (dbs_processing >= cluster->dbarr.ndbs)
+				return;
+
+			/*
+			 * Claim the next database in the cluster's array and initiate a
+			 * new connection.
+			 */
+			slot->db_idx = dbs_processing++;
+			slot->state = CONNECTING;
+			start_conn(cluster, slot);
+
+			return;
+
+		case CONNECTING:
+
+			/* Check for connection failure. */
+			if (PQstatus(slot->conn) == CONNECTION_BAD)
+				pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+
+			/* Check whether the connection is still establishing. */
+			if (PQstatus(slot->conn) != CONNECTION_OK)
+				return;
+
+			/*
+			 * Move on to running/processing the queries in the task.
+			 */
+			slot->state = RUNNING_QUERIES;
+			if (!PQsendQuery(slot->conn, task->queries->data))
+				pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+
+			return;
+
+		case RUNNING_QUERIES:
+
+			/*
+			 * Consume any available data and clear the read-ready indicator
+			 * for the connection.
+			 */
+			if (!PQconsumeInput(slot->conn))
+				pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+
+			/*
+			 * Process any results that are ready so that we can free up this
+			 * slot for another database as soon as possible.
+			 */
+			for (; slot->step_idx < task->num_steps; slot->step_idx++)
+			{
+				/* If no more results are available yet, move on. */
+				if (PQisBusy(slot->conn))
+					return;
+
+				process_query_result(cluster, slot, task);
+			}
+
+			/*
+			 * If we just finished processing the result of the last step in
+			 * the task, free the slot.  We recursively call this function on
+			 * the newly-freed slot so that we can start initiating the next
+			 * connection immediately instead of waiting for the next loop
+			 * through the slots.
+			 */
+			dbs_complete++;
+			PQfinish(slot->conn);
+			memset(slot, 0, sizeof(UpgradeTaskSlot));
+			slot->ready = true;
+
+			process_slot(cluster, slot, task);
+
+			return;
+	}
+}
+
+/*
+ * Returns -1 on error, else the number of ready descriptors.
+ */
+static int
+select_loop(int maxFd, fd_set *input, fd_set *output, bool nowait)
+{
+	fd_set		save_input = *input;
+	fd_set		save_output = *output;
+	struct timeval timeout = {0, 0};
+
+	if (maxFd == 0)
+		return 0;
+
+	for (;;)
+	{
+		int			i;
+
+		*input = save_input;
+		*output = save_output;
+
+		i = select(maxFd + 1, input, output, NULL, nowait ? &timeout : NULL);
+
+#ifndef WIN32
+		if (i < 0 && errno == EINTR)
+			continue;
+#else
+		if (i == SOCKET_ERROR && WSAGetLastError() == WSAEINTR)
+			continue;
+#endif
+		return i;
+	}
+}
+
+/*
+ * Wait on the slots to either finish connecting or to receive query results if
+ * possible.  This avoids a tight loop in upgrade_task_run().
+ */
+static void
+wait_on_slots(UpgradeTaskSlot *slots, int numslots)
+{
+	fd_set		input;
+	fd_set		output;
+	int			maxFd = 0;
+	bool		skip_wait = false;
+
+	FD_ZERO(&input);
+	FD_ZERO(&output);
+
+	for (int i = 0; i < numslots; i++)
+	{
+		PostgresPollingStatusType status;
+
+		switch (slots[i].state)
+		{
+			case FREE:
+
+				/*
+				 * This function should only ever see free slots as we are
+				 * finishing processing the last few databases, at which point
+				 * we don't have any databases left for them to process. We'll
+				 * never use these slots again, so we can safely ignore them.
+				 */
+				slots[i].ready = false;
+				continue;
+
+			case CONNECTING:
+
+				/*
+				 * Don't call PQconnectPoll() again for this slot until
+				 * select() tells us something is ready.  Be sure to use the
+				 * previous poll mode in this case.
+				 */
+				if (!slots[i].ready)
+					break;
+
+				/*
+				 * If we are waiting for the connection to establish, choose
+				 * whether to wait for reading or for writing on the socket as
+				 * appropriate.  If neither apply, mark the slot as ready and
+				 * skip waiting so that it is handled ASAP (we assume this
+				 * means the connection is either bad or fully ready).
+				 */
+				status = PQconnectPoll(slots[i].conn);
+				if (status == PGRES_POLLING_READING)
+					slots[i].select_mode = true;
+				else if (status == PGRES_POLLING_WRITING)
+					slots[i].select_mode = false;
+				else
+				{
+					slots[i].ready = true;
+					skip_wait = true;
+					continue;
+				}
+
+				break;
+
+			case RUNNING_QUERIES:
+
+				/*
+				 * Once we've sent the queries, we must wait for the socket to
+				 * be read-ready.  Note that process_slot() handles calling
+				 * PQconsumeInput() as required.
+				 */
+				slots[i].select_mode = true;
+				break;
+		}
+
+		/*
+		 * Add the socket to the set.
+		 */
+		slots[i].ready = false;
+		slots[i].sock = PQsocket(slots[i].conn);
+		if (slots[i].sock < 0)
+			pg_fatal("invalid socket");
+		FD_SET(slots[i].sock, slots[i].select_mode ? &input : &output);
+		maxFd = Max(maxFd, slots[i].sock);
+	}
+
+	/*
+	 * If we found socket(s) to wait on, wait.
+	 */
+	if (select_loop(maxFd, &input, &output, skip_wait) == -1)
+		pg_fatal("select() failed: %m");
+
+	/*
+	 * Mark which sockets appear to be ready.
+	 */
+	for (int i = 0; i < numslots; i++)
+		slots[i].ready |= (FD_ISSET(slots[i].sock, &input) ||
+						   FD_ISSET(slots[i].sock, &output));
+}
+
+/*
+ * Runs all the steps of the task in every database in the cluster using
+ * user_opts.jobs parallel slots.
+ */
+void
+upgrade_task_run(const UpgradeTask *task, const ClusterInfo *cluster)
+{
+	int			jobs = Max(1, user_opts.jobs);
+	UpgradeTaskSlot *slots = pg_malloc0(sizeof(UpgradeTaskSlot) * jobs);
+
+	dbs_complete = 0;
+	dbs_processing = 0;
+
+	/*
+	 * Process every slot the first time round.
+	 */
+	for (int i = 0; i < jobs; i++)
+		slots[i].ready = true;
+
+	while (dbs_complete < cluster->dbarr.ndbs)
+	{
+		for (int i = 0; i < jobs; i++)
+			process_slot(cluster, &slots[i], task);
+
+		wait_on_slots(slots, jobs);
+	}
+
+	pg_free(slots);
+}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index df3f336bec..725863f9c8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3040,6 +3040,11 @@ UnresolvedTup
 UnresolvedTupData
 UpdateContext
 UpdateStmt
+UpgradeTask
+UpgradeTaskReport
+UpgradeTaskSlot
+UpgradeTaskSlotState
+UpgradeTaskStep
 UploadManifestCmd
 UpperRelationKind
 UpperUniquePath
-- 
2.39.3 (Apple Git-146)

v12-0002-Use-pg_upgrade-s-new-parallel-framework-for-subs.patchtext/plain; charset=us-asciiDownload
From c148ee632d1fcb2f52c20a97ed0a569b3a20f88d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 17:21:19 -0500
Subject: [PATCH v12 02/11] Use pg_upgrade's new parallel framework for
 subscription checks.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 206 ++++++++++++++++++++-----------------
 1 file changed, 111 insertions(+), 95 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 96adea41e9..f8160e0140 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1905,6 +1905,38 @@ check_old_cluster_for_valid_slots(void)
 	check_ok();
 }
 
+/*
+ * Callback function for processing results of query for
+ * check_old_cluster_subscription_state()'s UpgradeTask.  If the query returned
+ * any rows (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_old_sub_state_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	int			ntup = PQntuples(res);
+	int			i_srsubstate = PQfnumber(res, "srsubstate");
+	int			i_subname = PQfnumber(res, "subname");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+
+	AssertVariableIsOfType(&process_old_sub_state_check, UpgradeTaskProcessCB);
+
+	for (int i = 0; i < ntup; i++)
+	{
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+
+		fprintf(report->file, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
+				PQgetvalue(res, i, i_srsubstate),
+				dbinfo->db_name,
+				PQgetvalue(res, i, i_subname),
+				PQgetvalue(res, i, i_nspname),
+				PQgetvalue(res, i, i_relname));
+	}
+}
+
 /*
  * check_old_cluster_subscription_state()
  *
@@ -1915,115 +1947,99 @@ check_old_cluster_for_valid_slots(void)
 static void
 check_old_cluster_subscription_state(void)
 {
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTask *task = upgrade_task_create();
+	UpgradeTaskReport report;
+	const char *query;
+	PGresult   *res;
+	PGconn	   *conn;
 	int			ntup;
 
 	prep_status("Checking for subscription state");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
 			 log_opts.basedir,
 			 "subs_invalid.txt");
-	for (int dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
-
-		/* We need to check for pg_replication_origin only once. */
-		if (dbnum == 0)
-		{
-			/*
-			 * Check that all the subscriptions have their respective
-			 * replication origin.
-			 */
-			res = executeQueryOrDie(conn,
-									"SELECT d.datname, s.subname "
-									"FROM pg_catalog.pg_subscription s "
-									"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
-									"	ON o.roname = 'pg_' || s.oid "
-									"INNER JOIN pg_catalog.pg_database d "
-									"	ON d.oid = s.subdbid "
-									"WHERE o.roname IS NULL;");
-
-			ntup = PQntuples(res);
-			for (int i = 0; i < ntup; i++)
-			{
-				if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-					pg_fatal("could not open file \"%s\": %m", output_path);
-				fprintf(script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
-						PQgetvalue(res, i, 0),
-						PQgetvalue(res, i, 1));
-			}
-			PQclear(res);
-		}
-
-		/*
-		 * We don't allow upgrade if there is a risk of dangling slot or
-		 * origin corresponding to initial sync after upgrade.
-		 *
-		 * A slot/origin not created yet refers to the 'i' (initialize) state,
-		 * while 'r' (ready) state refers to a slot/origin created previously
-		 * but already dropped. These states are supported for pg_upgrade. The
-		 * other states listed below are not supported:
-		 *
-		 * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state
-		 * would retain a replication slot, which could not be dropped by the
-		 * sync worker spawned after the upgrade because the subscription ID
-		 * used for the slot name won't match anymore.
-		 *
-		 * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state
-		 * would retain the replication origin when there is a failure in
-		 * tablesync worker immediately after dropping the replication slot in
-		 * the publisher.
-		 *
-		 * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on
-		 * a relation upgraded while in this state would expect an origin ID
-		 * with the OID of the subscription used before the upgrade, causing
-		 * it to fail.
-		 *
-		 * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and
-		 * SUBREL_STATE_UNKNOWN: These states are not stored in the catalog,
-		 * so we need not allow these states.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT r.srsubstate, s.subname, n.nspname, c.relname "
-								"FROM pg_catalog.pg_subscription_rel r "
-								"LEFT JOIN pg_catalog.pg_subscription s"
-								"	ON r.srsubid = s.oid "
-								"LEFT JOIN pg_catalog.pg_class c"
-								"	ON r.srrelid = c.oid "
-								"LEFT JOIN pg_catalog.pg_namespace n"
-								"	ON c.relnamespace = n.oid "
-								"WHERE r.srsubstate NOT IN ('i', 'r') "
-								"ORDER BY s.subname");
-
-		ntup = PQntuples(res);
-		for (int i = 0; i < ntup; i++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-
-			fprintf(script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
-					PQgetvalue(res, i, 0),
-					active_db->db_name,
-					PQgetvalue(res, i, 1),
-					PQgetvalue(res, i, 2),
-					PQgetvalue(res, i, 3));
-		}
 
-		PQclear(res);
-		PQfinish(conn);
+	/*
+	 * Check that all the subscriptions have their respective replication
+	 * origin.  This check only needs to run once.
+	 */
+	conn = connectToServer(&old_cluster, old_cluster.dbarr.dbs[0].db_name);
+	res = executeQueryOrDie(conn,
+							"SELECT d.datname, s.subname "
+							"FROM pg_catalog.pg_subscription s "
+							"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
+							"	ON o.roname = 'pg_' || s.oid "
+							"INNER JOIN pg_catalog.pg_database d "
+							"	ON d.oid = s.subdbid "
+							"WHERE o.roname IS NULL;");
+	ntup = PQntuples(res);
+	for (int i = 0; i < ntup; i++)
+	{
+		if (report.file == NULL &&
+			(report.file = fopen_priv(report.path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report.path);
+		fprintf(report.file, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
+				PQgetvalue(res, i, 0),
+				PQgetvalue(res, i, 1));
 	}
+	PQclear(res);
+	PQfinish(conn);
 
-	if (script)
+	/*
+	 * We don't allow upgrade if there is a risk of dangling slot or origin
+	 * corresponding to initial sync after upgrade.
+	 *
+	 * A slot/origin not created yet refers to the 'i' (initialize) state,
+	 * while 'r' (ready) state refers to a slot/origin created previously but
+	 * already dropped. These states are supported for pg_upgrade. The other
+	 * states listed below are not supported:
+	 *
+	 * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state would
+	 * retain a replication slot, which could not be dropped by the sync
+	 * worker spawned after the upgrade because the subscription ID used for
+	 * the slot name won't match anymore.
+	 *
+	 * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state would
+	 * retain the replication origin when there is a failure in tablesync
+	 * worker immediately after dropping the replication slot in the
+	 * publisher.
+	 *
+	 * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on a
+	 * relation upgraded while in this state would expect an origin ID with
+	 * the OID of the subscription used before the upgrade, causing it to
+	 * fail.
+	 *
+	 * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and
+	 * SUBREL_STATE_UNKNOWN: These states are not stored in the catalog, so we
+	 * need not allow these states.
+	 */
+	query = "SELECT r.srsubstate, s.subname, n.nspname, c.relname "
+		"FROM pg_catalog.pg_subscription_rel r "
+		"LEFT JOIN pg_catalog.pg_subscription s"
+		"   ON r.srsubid = s.oid "
+		"LEFT JOIN pg_catalog.pg_class c"
+		"   ON r.srrelid = c.oid "
+		"LEFT JOIN pg_catalog.pg_namespace n"
+		"   ON c.relnamespace = n.oid "
+		"WHERE r.srsubstate NOT IN ('i', 'r') "
+		"ORDER BY s.subname";
+
+	upgrade_task_add_step(task, query, process_old_sub_state_check,
+						  true, &report);
+
+	upgrade_task_run(task, &old_cluster);
+	upgrade_task_free(task);
+
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains subscriptions without origin or having relations not in i (initialize) or r (ready) state.\n"
 				 "You can allow the initial sync to finish for all relations and then restart the upgrade.\n"
 				 "A list of the problematic subscriptions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v12-0003-Use-pg_upgrade-s-new-parallel-framework-to-get-r.patchtext/plain; charset=us-asciiDownload
From 75ff88b58a1b98429a253f4f45efc28c529dc6e7 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:09:33 -0500
Subject: [PATCH v12 03/11] Use pg_upgrade's new parallel framework to get
 relation info.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/info.c | 296 ++++++++++++++++++++------------------
 1 file changed, 154 insertions(+), 142 deletions(-)

diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index d3c1e8918d..2bfc8dcfba 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -11,6 +11,7 @@
 
 #include "access/transam.h"
 #include "catalog/pg_class_d.h"
+#include "pqexpbuffer.h"
 #include "pg_upgrade.h"
 
 static void create_rel_filename_map(const char *old_data, const char *new_data,
@@ -22,12 +23,14 @@ static void report_unmatched_relation(const RelInfo *rel, const DbInfo *db,
 static void free_db_and_rel_infos(DbInfoArr *db_arr);
 static void get_template0_info(ClusterInfo *cluster);
 static void get_db_infos(ClusterInfo *cluster);
-static void get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo);
+static char *get_rel_infos_query(void);
+static void process_rel_infos(DbInfo *dbinfo, PGresult *res, void *arg);
 static void free_rel_infos(RelInfoArr *rel_arr);
 static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
-static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo);
+static char *get_old_cluster_logical_slot_infos_query(void);
+static void process_old_cluster_logical_slot_infos(DbInfo *dbinfo, PGresult *res, void *arg);
 
 
 /*
@@ -276,7 +279,9 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
 void
 get_db_rel_and_slot_infos(ClusterInfo *cluster)
 {
-	int			dbnum;
+	UpgradeTask *task = upgrade_task_create();
+	char	   *rel_infos_query = NULL;
+	char	   *logical_slot_infos_query = NULL;
 
 	if (cluster->dbarr.dbs != NULL)
 		free_db_and_rel_infos(&cluster->dbarr);
@@ -284,15 +289,37 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster)
 	get_template0_info(cluster);
 	get_db_infos(cluster);
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	rel_infos_query = get_rel_infos_query();
+	upgrade_task_add_step(task,
+						  rel_infos_query,
+						  process_rel_infos,
+						  true, NULL);
+
+	/*
+	 * Logical slots are only carried over to the new cluster when the old
+	 * cluster is on PG17 or newer.  This is because before that the logical
+	 * slots are not saved at shutdown, so there is no guarantee that the
+	 * latest confirmed_flush_lsn is saved to disk which can lead to data
+	 * loss. It is still not guaranteed for manually created slots in PG17, so
+	 * subsequent checks done in check_old_cluster_for_valid_slots() would
+	 * raise a FATAL error if such slots are included.
+	 */
+	if (cluster == &old_cluster &&
+		GET_MAJOR_VERSION(cluster->major_version) > 1600)
 	{
-		DbInfo	   *pDbInfo = &cluster->dbarr.dbs[dbnum];
+		logical_slot_infos_query = get_old_cluster_logical_slot_infos_query();
+		upgrade_task_add_step(task,
+							  logical_slot_infos_query,
+							  process_old_cluster_logical_slot_infos,
+							  true, NULL);
+	}
 
-		get_rel_infos(cluster, pDbInfo);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
 
-		if (cluster == &old_cluster)
-			get_old_cluster_logical_slot_infos(pDbInfo);
-	}
+	pg_free(rel_infos_query);
+	if (logical_slot_infos_query)
+		pg_free(logical_slot_infos_query);
 
 	if (cluster == &old_cluster)
 		pg_log(PG_VERBOSE, "\nsource databases:");
@@ -431,40 +458,21 @@ get_db_infos(ClusterInfo *cluster)
 
 
 /*
- * get_rel_infos()
+ * get_rel_infos_query()
  *
- * gets the relinfos for all the user tables and indexes of the database
- * referred to by "dbinfo".
+ * Returns the query for retrieving the relation information for all the user
+ * tables and indexes in the database, for use by get_db_rel_and_slot_infos()'s
+ * UpgradeTask.
  *
- * Note: the resulting RelInfo array is assumed to be sorted by OID.
- * This allows later processing to match up old and new databases efficiently.
+ * Note: the result is assumed to be sorted by OID.  This allows later
+ * processing to match up old and new databases efficiently.
  */
-static void
-get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
+static char *
+get_rel_infos_query(void)
 {
-	PGconn	   *conn = connectToServer(cluster,
-									   dbinfo->db_name);
-	PGresult   *res;
-	RelInfo    *relinfos;
-	int			ntups;
-	int			relnum;
-	int			num_rels = 0;
-	char	   *nspname = NULL;
-	char	   *relname = NULL;
-	char	   *tablespace = NULL;
-	int			i_spclocation,
-				i_nspname,
-				i_relname,
-				i_reloid,
-				i_indtable,
-				i_toastheap,
-				i_relfilenumber,
-				i_reltablespace;
-	char		query[QUERY_ALLOC];
-	char	   *last_namespace = NULL,
-			   *last_tablespace = NULL;
+	PQExpBufferData query;
 
-	query[0] = '\0';			/* initialize query string to empty */
+	initPQExpBuffer(&query);
 
 	/*
 	 * Create a CTE that collects OIDs of regular user tables and matviews,
@@ -476,34 +484,34 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * output, so we have to copy that system table.  It's easiest to do that
 	 * by treating it as a user table.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "WITH regular_heap (reloid, indtable, toastheap) AS ( "
-			 "  SELECT c.oid, 0::oid, 0::oid "
-			 "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
-			 "         ON c.relnamespace = n.oid "
-			 "  WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
-			 CppAsString2(RELKIND_MATVIEW) ") AND "
+	appendPQExpBuffer(&query,
+					  "WITH regular_heap (reloid, indtable, toastheap) AS ( "
+					  "  SELECT c.oid, 0::oid, 0::oid "
+					  "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
+					  "         ON c.relnamespace = n.oid "
+					  "  WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
+					  CppAsString2(RELKIND_MATVIEW) ") AND "
 	/* exclude possible orphaned temp tables */
-			 "    ((n.nspname !~ '^pg_temp_' AND "
-			 "      n.nspname !~ '^pg_toast_temp_' AND "
-			 "      n.nspname NOT IN ('pg_catalog', 'information_schema', "
-			 "                        'binary_upgrade', 'pg_toast') AND "
-			 "      c.oid >= %u::pg_catalog.oid) OR "
-			 "     (n.nspname = 'pg_catalog' AND "
-			 "      relname IN ('pg_largeobject') ))), ",
-			 FirstNormalObjectId);
+					  "    ((n.nspname !~ '^pg_temp_' AND "
+					  "      n.nspname !~ '^pg_toast_temp_' AND "
+					  "      n.nspname NOT IN ('pg_catalog', 'information_schema', "
+					  "                        'binary_upgrade', 'pg_toast') AND "
+					  "      c.oid >= %u::pg_catalog.oid) OR "
+					  "     (n.nspname = 'pg_catalog' AND "
+					  "      relname IN ('pg_largeobject') ))), ",
+					  FirstNormalObjectId);
 
 	/*
 	 * Add a CTE that collects OIDs of toast tables belonging to the tables
 	 * selected by the regular_heap CTE.  (We have to do this separately
 	 * because the namespace-name rules above don't work for toast tables.)
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "  toast_heap (reloid, indtable, toastheap) AS ( "
-			 "  SELECT c.reltoastrelid, 0::oid, c.oid "
-			 "  FROM regular_heap JOIN pg_catalog.pg_class c "
-			 "      ON regular_heap.reloid = c.oid "
-			 "  WHERE c.reltoastrelid != 0), ");
+	appendPQExpBufferStr(&query,
+						 "  toast_heap (reloid, indtable, toastheap) AS ( "
+						 "  SELECT c.reltoastrelid, 0::oid, c.oid "
+						 "  FROM regular_heap JOIN pg_catalog.pg_class c "
+						 "      ON regular_heap.reloid = c.oid "
+						 "  WHERE c.reltoastrelid != 0), ");
 
 	/*
 	 * Add a CTE that collects OIDs of all valid indexes on the previously
@@ -511,53 +519,68 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * Testing indisready is necessary in 9.2, and harmless in earlier/later
 	 * versions.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "  all_index (reloid, indtable, toastheap) AS ( "
-			 "  SELECT indexrelid, indrelid, 0::oid "
-			 "  FROM pg_catalog.pg_index "
-			 "  WHERE indisvalid AND indisready "
-			 "    AND indrelid IN "
-			 "        (SELECT reloid FROM regular_heap "
-			 "         UNION ALL "
-			 "         SELECT reloid FROM toast_heap)) ");
+	appendPQExpBufferStr(&query,
+						 "  all_index (reloid, indtable, toastheap) AS ( "
+						 "  SELECT indexrelid, indrelid, 0::oid "
+						 "  FROM pg_catalog.pg_index "
+						 "  WHERE indisvalid AND indisready "
+						 "    AND indrelid IN "
+						 "        (SELECT reloid FROM regular_heap "
+						 "         UNION ALL "
+						 "         SELECT reloid FROM toast_heap)) ");
 
 	/*
 	 * And now we can write the query that retrieves the data we want for each
 	 * heap and index relation.  Make sure result is sorted by OID.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "SELECT all_rels.*, n.nspname, c.relname, "
-			 "  c.relfilenode, c.reltablespace, "
-			 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
-			 "FROM (SELECT * FROM regular_heap "
-			 "      UNION ALL "
-			 "      SELECT * FROM toast_heap "
-			 "      UNION ALL "
-			 "      SELECT * FROM all_index) all_rels "
-			 "  JOIN pg_catalog.pg_class c "
-			 "      ON all_rels.reloid = c.oid "
-			 "  JOIN pg_catalog.pg_namespace n "
-			 "     ON c.relnamespace = n.oid "
-			 "  LEFT OUTER JOIN pg_catalog.pg_tablespace t "
-			 "     ON c.reltablespace = t.oid "
-			 "ORDER BY 1;");
-
-	res = executeQueryOrDie(conn, "%s", query);
-
-	ntups = PQntuples(res);
+	appendPQExpBufferStr(&query,
+						 "SELECT all_rels.*, n.nspname, c.relname, "
+						 "  c.relfilenode, c.reltablespace, "
+						 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
+						 "FROM (SELECT * FROM regular_heap "
+						 "      UNION ALL "
+						 "      SELECT * FROM toast_heap "
+						 "      UNION ALL "
+						 "      SELECT * FROM all_index) all_rels "
+						 "  JOIN pg_catalog.pg_class c "
+						 "      ON all_rels.reloid = c.oid "
+						 "  JOIN pg_catalog.pg_namespace n "
+						 "     ON c.relnamespace = n.oid "
+						 "  LEFT OUTER JOIN pg_catalog.pg_tablespace t "
+						 "     ON c.reltablespace = t.oid "
+						 "ORDER BY 1;");
+
+	return query.data;
+}
 
-	relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+/*
+ * Callback function for processing results of the query returned by
+ * get_rel_infos_query(), which is used for get_db_rel_and_slot_infos()'s
+ * UpgradeTask.  This function stores the relation information for later use.
+ */
+static void
+process_rel_infos(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	int			ntups = PQntuples(res);
+	RelInfo    *relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	int			i_reloid = PQfnumber(res, "reloid");
+	int			i_indtable = PQfnumber(res, "indtable");
+	int			i_toastheap = PQfnumber(res, "toastheap");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+	int			i_relfilenumber = PQfnumber(res, "relfilenode");
+	int			i_reltablespace = PQfnumber(res, "reltablespace");
+	int			i_spclocation = PQfnumber(res, "spclocation");
+	int			num_rels = 0;
+	char	   *nspname = NULL;
+	char	   *relname = NULL;
+	char	   *tablespace = NULL;
+	char	   *last_namespace = NULL;
+	char	   *last_tablespace = NULL;
 
-	i_reloid = PQfnumber(res, "reloid");
-	i_indtable = PQfnumber(res, "indtable");
-	i_toastheap = PQfnumber(res, "toastheap");
-	i_nspname = PQfnumber(res, "nspname");
-	i_relname = PQfnumber(res, "relname");
-	i_relfilenumber = PQfnumber(res, "relfilenode");
-	i_reltablespace = PQfnumber(res, "reltablespace");
-	i_spclocation = PQfnumber(res, "spclocation");
+	AssertVariableIsOfType(&process_rel_infos, UpgradeTaskProcessCB);
 
-	for (relnum = 0; relnum < ntups; relnum++)
+	for (int relnum = 0; relnum < ntups; relnum++)
 	{
 		RelInfo    *curr = &relinfos[num_rels++];
 
@@ -610,44 +633,22 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 			/* A zero reltablespace oid indicates the database tablespace. */
 			curr->tablespace = dbinfo->db_tablespace;
 	}
-	PQclear(res);
-
-	PQfinish(conn);
 
 	dbinfo->rel_arr.rels = relinfos;
 	dbinfo->rel_arr.nrels = num_rels;
 }
 
 /*
- * get_old_cluster_logical_slot_infos()
- *
- * Gets the LogicalSlotInfos for all the logical replication slots of the
- * database referred to by "dbinfo". The status of each logical slot is gotten
- * here, but they are used at the checking phase. See
- * check_old_cluster_for_valid_slots().
+ * get_old_cluster_logical_slot_infos_query()
  *
- * Note: This function will not do anything if the old cluster is pre-PG17.
- * This is because before that the logical slots are not saved at shutdown, so
- * there is no guarantee that the latest confirmed_flush_lsn is saved to disk
- * which can lead to data loss. It is still not guaranteed for manually created
- * slots in PG17, so subsequent checks done in
- * check_old_cluster_for_valid_slots() would raise a FATAL error if such slots
- * are included.
+ * Returns the query for retrieving the logical slot information for all the
+ * logical replication slots in the database, for use by
+ * get_db_rel_and_slot_infos()'s UpgradeTask.  The status of each logical slot
+ * is checked in check_old_cluster_for_valid_slots().
  */
-static void
-get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
+static char *
+get_old_cluster_logical_slot_infos_query(void)
 {
-	PGconn	   *conn;
-	PGresult   *res;
-	LogicalSlotInfo *slotinfos = NULL;
-	int			num_slots;
-
-	/* Logical slots can be migrated since PG17. */
-	if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1600)
-		return;
-
-	conn = connectToServer(&old_cluster, dbinfo->db_name);
-
 	/*
 	 * Fetch the logical replication slot information. The check whether the
 	 * slot is considered caught up is done by an upgrade function. This
@@ -665,18 +666,32 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
-							"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
-							"FROM pg_catalog.pg_replication_slots "
-							"WHERE slot_type = 'logical' AND "
-							"database = current_database() AND "
-							"temporary IS FALSE;",
-							user_opts.live_check ? "FALSE" :
-							"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
-							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
-							"END)");
-
-	num_slots = PQntuples(res);
+	return psprintf("SELECT slot_name, plugin, two_phase, failover, "
+					"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
+					"FROM pg_catalog.pg_replication_slots "
+					"WHERE slot_type = 'logical' AND "
+					"database = current_database() AND "
+					"temporary IS FALSE;",
+					user_opts.live_check ? "FALSE" :
+					"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
+					"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
+					"END)");
+}
+
+/*
+ * Callback function for processing results of the query returned by
+ * get_old_cluster_logical_slot_infos_query(), which is used for
+ * get_db_rel_and_slot_infos()'s UpgradeTask.  This function stores the logical
+ * slot information for later use.
+ */
+static void
+process_old_cluster_logical_slot_infos(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	LogicalSlotInfo *slotinfos = NULL;
+	int			num_slots = PQntuples(res);
+
+	AssertVariableIsOfType(&process_old_cluster_logical_slot_infos,
+						   UpgradeTaskProcessCB);
 
 	if (num_slots)
 	{
@@ -709,9 +724,6 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 		}
 	}
 
-	PQclear(res);
-	PQfinish(conn);
-
 	dbinfo->slot_arr.slots = slotinfos;
 	dbinfo->slot_arr.nslots = num_slots;
 }
-- 
2.39.3 (Apple Git-146)

v12-0004-Use-pg_upgrade-s-new-parallel-framework-to-get-l.patchtext/plain; charset=us-asciiDownload
From cf6a6aa9b2eb91ed9e20ac4082e5920a34a8a411 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 13:32:33 -0500
Subject: [PATCH v12 04/11] Use pg_upgrade's new parallel framework to get
 loadable libraries.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/function.c | 71 ++++++++++++++++++++++-------------
 1 file changed, 45 insertions(+), 26 deletions(-)

diff --git a/src/bin/pg_upgrade/function.c b/src/bin/pg_upgrade/function.c
index 7e3abed098..0588347b49 100644
--- a/src/bin/pg_upgrade/function.c
+++ b/src/bin/pg_upgrade/function.c
@@ -42,6 +42,30 @@ library_name_compare(const void *p1, const void *p2)
 					  ((const LibraryInfo *) p2)->dbnum);
 }
 
+/*
+ * Private state for get_loadable_libraries()'s UpgradeTask.
+ */
+struct loadable_libraries_state
+{
+	PGresult  **ress;			/* results for each database */
+	int			totaltups;		/* number of tuples in all results */
+};
+
+/*
+ * Callback function for processing results of query for
+ * get_loadable_libraries()'s UpgradeTask.  This function stores the results
+ * for later use within get_loadable_libraries().
+ */
+static void
+process_loadable_libraries(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct loadable_libraries_state *state = (struct loadable_libraries_state *) arg;
+
+	AssertVariableIsOfType(&process_loadable_libraries, UpgradeTaskProcessCB);
+
+	state->ress[dbinfo - old_cluster.dbarr.dbs] = res;
+	state->totaltups += PQntuples(res);
+}
 
 /*
  * get_loadable_libraries()
@@ -54,47 +78,41 @@ library_name_compare(const void *p1, const void *p2)
 void
 get_loadable_libraries(void)
 {
-	PGresult  **ress;
 	int			totaltups;
 	int			dbnum;
 	int			n_libinfos;
+	UpgradeTask *task = upgrade_task_create();
+	struct loadable_libraries_state state;
+	char	   *query;
 
-	ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
-	totaltups = 0;
+	state.ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
+	state.totaltups = 0;
 
-	/* Fetch all library names, removing duplicates within each DB */
-	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
+	query = psprintf("SELECT DISTINCT probin "
+					 "FROM pg_catalog.pg_proc "
+					 "WHERE prolang = %u AND "
+					 "probin IS NOT NULL AND "
+					 "oid >= %u;",
+					 ClanguageId,
+					 FirstNormalObjectId);
 
-		/*
-		 * Fetch all libraries containing non-built-in C functions in this DB.
-		 */
-		ress[dbnum] = executeQueryOrDie(conn,
-										"SELECT DISTINCT probin "
-										"FROM pg_catalog.pg_proc "
-										"WHERE prolang = %u AND "
-										"probin IS NOT NULL AND "
-										"oid >= %u;",
-										ClanguageId,
-										FirstNormalObjectId);
-		totaltups += PQntuples(ress[dbnum]);
-
-		PQfinish(conn);
-	}
+	upgrade_task_add_step(task, query, process_loadable_libraries,
+						  false, &state);
+
+	upgrade_task_run(task, &old_cluster);
+	upgrade_task_free(task);
 
 	/*
 	 * Allocate memory for required libraries and logical replication output
 	 * plugins.
 	 */
-	n_libinfos = totaltups + count_old_cluster_logical_slots();
+	n_libinfos = state.totaltups + count_old_cluster_logical_slots();
 	os_info.libraries = (LibraryInfo *) pg_malloc(sizeof(LibraryInfo) * n_libinfos);
 	totaltups = 0;
 
 	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
 	{
-		PGresult   *res = ress[dbnum];
+		PGresult   *res = state.ress[dbnum];
 		int			ntups;
 		int			rowno;
 		LogicalSlotInfoArr *slot_arr = &old_cluster.dbarr.dbs[dbnum].slot_arr;
@@ -129,7 +147,8 @@ get_loadable_libraries(void)
 		}
 	}
 
-	pg_free(ress);
+	pg_free(state.ress);
+	pg_free(query);
 
 	os_info.num_libraries = totaltups;
 }
-- 
2.39.3 (Apple Git-146)

v12-0005-Use-pg_upgrade-s-new-parallel-framework-for-exte.patchtext/plain; charset=us-asciiDownload
From 38dfb85077112ebeea07bffc0e96af0fa148e20a Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 14:18:39 -0500
Subject: [PATCH v12 05/11] Use pg_upgrade's new parallel framework for
 extension updates.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/version.c | 94 +++++++++++++++++++-----------------
 1 file changed, 49 insertions(+), 45 deletions(-)

diff --git a/src/bin/pg_upgrade/version.c b/src/bin/pg_upgrade/version.c
index 2de6dffccd..5084b08805 100644
--- a/src/bin/pg_upgrade/version.c
+++ b/src/bin/pg_upgrade/version.c
@@ -139,6 +139,41 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 		check_ok();
 }
 
+/*
+ * Callback function for processing results of query for
+ * report_extension_updates()'s UpgradeTask.  If the query returned any rows,
+ * write the details to the report file.
+ */
+static void
+process_extension_updates(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_name = PQfnumber(res, "name");
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+
+	AssertVariableIsOfType(&process_extension_updates, UpgradeTaskProcessCB);
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
+		{
+			PQExpBufferData connectbuf;
+
+			initPQExpBuffer(&connectbuf);
+			appendPsqlMetaConnect(&connectbuf, dbinfo->db_name);
+			fputs(connectbuf.data, report->file);
+			termPQExpBuffer(&connectbuf);
+			db_used = true;
+		}
+		fprintf(report->file, "ALTER EXTENSION %s UPDATE;\n",
+				quote_identifier(PQgetvalue(res, rowno, i_name)));
+	}
+}
+
 /*
  * report_extension_updates()
  *	Report extensions that should be updated.
@@ -146,57 +181,26 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 void
 report_extension_updates(ClusterInfo *cluster)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char	   *output_path = "update_extensions.sql";
+	UpgradeTaskReport report;
+	UpgradeTask *task = upgrade_task_create();
+	const char *query = "SELECT name "
+		"FROM pg_available_extensions "
+		"WHERE installed_version != default_version";
 
 	prep_status("Checking for extension updates");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_name;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* find extensions needing updates */
-		res = executeQueryOrDie(conn,
-								"SELECT name "
-								"FROM pg_available_extensions "
-								"WHERE installed_version != default_version"
-			);
+	report.file = NULL;
+	strcpy(report.path, "update_extensions.sql");
 
-		ntups = PQntuples(res);
-		i_name = PQfnumber(res, "name");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				PQExpBufferData connectbuf;
+	upgrade_task_add_step(task, query, process_extension_updates,
+						  true, &report);
 
-				initPQExpBuffer(&connectbuf);
-				appendPsqlMetaConnect(&connectbuf, active_db->db_name);
-				fputs(connectbuf.data, script);
-				termPQExpBuffer(&connectbuf);
-				db_used = true;
-			}
-			fprintf(script, "ALTER EXTENSION %s UPDATE;\n",
-					quote_identifier(PQgetvalue(res, rowno, i_name)));
-		}
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
 
-		PQclear(res);
-
-		PQfinish(conn);
-	}
-
-	if (script)
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		report_status(PG_REPORT, "notice");
 		pg_log(PG_REPORT, "\n"
 			   "Your installation contains extensions that should be updated\n"
@@ -204,7 +208,7 @@ report_extension_updates(ClusterInfo *cluster)
 			   "    %s\n"
 			   "when executed by psql by the database superuser will update\n"
 			   "these extensions.",
-			   output_path);
+			   report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v12-0006-Use-pg_upgrade-s-new-parallel-framework-for-data.patchtext/plain; charset=us-asciiDownload
From e54e8043034cfd909db89da97eadb601d75cd710 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Sat, 6 Jul 2024 21:06:31 -0500
Subject: [PATCH v12 06/11] Use pg_upgrade's new parallel framework for data
 type checks.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 351 ++++++++++++++++++++-----------------
 1 file changed, 191 insertions(+), 160 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index f8160e0140..f935b53e1f 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -314,6 +314,147 @@ static DataTypesUsageChecks data_types_usage_checks[] =
 	}
 };
 
+/*
+ * Private state for check_for_data_types_usage()'s UpgradeTask.
+ */
+struct data_type_check_state
+{
+	DataTypesUsageChecks *check;	/* the check for this step */
+	bool	   *result;			/* true if check failed for any database */
+	PQExpBuffer *report;		/* buffer for report on failed checks */
+};
+
+/*
+ * Returns a palloc'd query string for the data type check, for use by
+ * check_for_data_types_usage()'s UpgradeTask.
+ */
+static char *
+data_type_check_query(int checknum)
+{
+	DataTypesUsageChecks *check = &data_types_usage_checks[checknum];
+
+	return psprintf("WITH RECURSIVE oids AS ( "
+	/* start with the type(s) returned by base_query */
+					"	%s "
+					"	UNION ALL "
+					"	SELECT * FROM ( "
+	/* inner WITH because we can only reference the CTE once */
+					"		WITH x AS (SELECT oid FROM oids) "
+	/* domains on any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
+					"			UNION ALL "
+	/* arrays over any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
+					"			UNION ALL "
+	/* composite types containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
+					"			WHERE t.typtype = 'c' AND "
+					"				  t.oid = c.reltype AND "
+					"				  c.oid = a.attrelid AND "
+					"				  NOT a.attisdropped AND "
+					"				  a.atttypid = x.oid "
+					"			UNION ALL "
+	/* ranges containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
+					"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
+					"	) foo "
+					") "
+	/* now look for stored columns of any such type */
+					"SELECT n.nspname, c.relname, a.attname "
+					"FROM	pg_catalog.pg_class c, "
+					"		pg_catalog.pg_namespace n, "
+					"		pg_catalog.pg_attribute a "
+					"WHERE	c.oid = a.attrelid AND "
+					"		NOT a.attisdropped AND "
+					"		a.atttypid IN (SELECT oid FROM oids) AND "
+					"		c.relkind IN ("
+					CppAsString2(RELKIND_RELATION) ", "
+					CppAsString2(RELKIND_MATVIEW) ", "
+					CppAsString2(RELKIND_INDEX) ") AND "
+					"		c.relnamespace = n.oid AND "
+	/* exclude possible orphaned temp tables */
+					"		n.nspname !~ '^pg_temp_' AND "
+					"		n.nspname !~ '^pg_toast_temp_' AND "
+	/* exclude system catalogs, too */
+					"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
+					check->base_query);
+}
+
+/*
+ * Callback function for processing results of queries for
+ * check_for_data_types_usage()'s UpgradeTask.  If the query returned any rows
+ * (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_data_type_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct data_type_check_state *state = (struct data_type_check_state *) arg;
+	int			ntups = PQntuples(res);
+
+	AssertVariableIsOfType(&process_data_type_check, UpgradeTaskProcessCB);
+
+	if (ntups)
+	{
+		char		output_path[MAXPGPATH];
+		int			i_nspname;
+		int			i_relname;
+		int			i_attname;
+		FILE	   *script = NULL;
+		bool		db_used = false;
+
+		snprintf(output_path, sizeof(output_path), "%s/%s",
+				 log_opts.basedir,
+				 state->check->report_filename);
+
+		/*
+		 * Make sure we have a buffer to save reports to now that we found a
+		 * first failing check.
+		 */
+		if (*state->report == NULL)
+			*state->report = createPQExpBuffer();
+
+		/*
+		 * If this is the first time we see an error for the check in question
+		 * then print a status message of the failure.
+		 */
+		if (!(*state->result))
+		{
+			pg_log(PG_REPORT, "    failed check: %s", _(state->check->status));
+			appendPQExpBuffer(*state->report, "\n%s\n%s    %s\n",
+							  _(state->check->report_text),
+							  _("A list of the problem columns is in the file:"),
+							  output_path);
+		}
+		*state->result = true;
+
+		i_nspname = PQfnumber(res, "nspname");
+		i_relname = PQfnumber(res, "relname");
+		i_attname = PQfnumber(res, "attname");
+
+		for (int rowno = 0; rowno < ntups; rowno++)
+		{
+			if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
+				pg_fatal("could not open file \"%s\": %m", output_path);
+
+			if (!db_used)
+			{
+				fprintf(script, "In database: %s\n", dbinfo->db_name);
+				db_used = true;
+			}
+			fprintf(script, "  %s.%s.%s\n",
+					PQgetvalue(res, rowno, i_nspname),
+					PQgetvalue(res, rowno, i_relname),
+					PQgetvalue(res, rowno, i_attname));
+		}
+
+		if (script)
+		{
+			fclose(script);
+			script = NULL;
+		}
+	}
+}
+
 /*
  * check_for_data_types_usage()
  *	Detect whether there are any stored columns depending on given type(s)
@@ -334,13 +475,15 @@ static DataTypesUsageChecks data_types_usage_checks[] =
  * there's no storage involved in a view.
  */
 static void
-check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
+check_for_data_types_usage(ClusterInfo *cluster)
 {
-	bool		found = false;
 	bool	   *results;
-	PQExpBufferData report;
-	DataTypesUsageChecks *tmp = checks;
+	PQExpBuffer report = NULL;
+	DataTypesUsageChecks *tmp = data_types_usage_checks;
 	int			n_data_types_usage_checks = 0;
+	UpgradeTask *task = upgrade_task_create();
+	char	  **queries = NULL;
+	struct data_type_check_state *states;
 
 	prep_status("Checking data type usage");
 
@@ -353,175 +496,63 @@ check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
 
 	/* Prepare an array to store the results of checks in */
 	results = pg_malloc0(sizeof(bool) * n_data_types_usage_checks);
+	queries = pg_malloc0(sizeof(char *) * n_data_types_usage_checks);
+	states = pg_malloc0(sizeof(struct data_type_check_state) * n_data_types_usage_checks);
 
-	/*
-	 * Connect to each database in the cluster and run all defined checks
-	 * against that database before trying the next one.
-	 */
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int i = 0; i < n_data_types_usage_checks; i++)
 	{
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
+		DataTypesUsageChecks *check = &data_types_usage_checks[i];
 
-		for (int checknum = 0; checknum < n_data_types_usage_checks; checknum++)
+		if (check->threshold_version == MANUAL_CHECK)
 		{
-			PGresult   *res;
-			int			ntups;
-			int			i_nspname;
-			int			i_relname;
-			int			i_attname;
-			FILE	   *script = NULL;
-			bool		db_used = false;
-			char		output_path[MAXPGPATH];
-			DataTypesUsageChecks *cur_check = &checks[checknum];
-
-			if (cur_check->threshold_version == MANUAL_CHECK)
-			{
-				Assert(cur_check->version_hook);
-
-				/*
-				 * Make sure that the check applies to the current cluster
-				 * version and skip if not. If no check hook has been defined
-				 * we run the check for all versions.
-				 */
-				if (!cur_check->version_hook(cluster))
-					continue;
-			}
-			else if (cur_check->threshold_version != ALL_VERSIONS)
-			{
-				if (GET_MAJOR_VERSION(cluster->major_version) > cur_check->threshold_version)
-					continue;
-			}
-			else
-				Assert(cur_check->threshold_version == ALL_VERSIONS);
-
-			snprintf(output_path, sizeof(output_path), "%s/%s",
-					 log_opts.basedir,
-					 cur_check->report_filename);
+			Assert(check->version_hook);
 
 			/*
-			 * The type(s) of interest might be wrapped in a domain, array,
-			 * composite, or range, and these container types can be nested
-			 * (to varying extents depending on server version, but that's not
-			 * of concern here).  To handle all these cases we need a
-			 * recursive CTE.
+			 * Make sure that the check applies to the current cluster version
+			 * and skip it if not.
 			 */
-			res = executeQueryOrDie(conn,
-									"WITH RECURSIVE oids AS ( "
-			/* start with the type(s) returned by base_query */
-									"	%s "
-									"	UNION ALL "
-									"	SELECT * FROM ( "
-			/* inner WITH because we can only reference the CTE once */
-									"		WITH x AS (SELECT oid FROM oids) "
-			/* domains on any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
-									"			UNION ALL "
-			/* arrays over any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
-									"			UNION ALL "
-			/* composite types containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
-									"			WHERE t.typtype = 'c' AND "
-									"				  t.oid = c.reltype AND "
-									"				  c.oid = a.attrelid AND "
-									"				  NOT a.attisdropped AND "
-									"				  a.atttypid = x.oid "
-									"			UNION ALL "
-			/* ranges containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
-									"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
-									"	) foo "
-									") "
-			/* now look for stored columns of any such type */
-									"SELECT n.nspname, c.relname, a.attname "
-									"FROM	pg_catalog.pg_class c, "
-									"		pg_catalog.pg_namespace n, "
-									"		pg_catalog.pg_attribute a "
-									"WHERE	c.oid = a.attrelid AND "
-									"		NOT a.attisdropped AND "
-									"		a.atttypid IN (SELECT oid FROM oids) AND "
-									"		c.relkind IN ("
-									CppAsString2(RELKIND_RELATION) ", "
-									CppAsString2(RELKIND_MATVIEW) ", "
-									CppAsString2(RELKIND_INDEX) ") AND "
-									"		c.relnamespace = n.oid AND "
-			/* exclude possible orphaned temp tables */
-									"		n.nspname !~ '^pg_temp_' AND "
-									"		n.nspname !~ '^pg_toast_temp_' AND "
-			/* exclude system catalogs, too */
-									"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
-									cur_check->base_query);
-
-			ntups = PQntuples(res);
+			if (!check->version_hook(cluster))
+				continue;
+		}
+		else if (check->threshold_version != ALL_VERSIONS)
+		{
+			if (GET_MAJOR_VERSION(cluster->major_version) > check->threshold_version)
+				continue;
+		}
+		else
+			Assert(check->threshold_version == ALL_VERSIONS);
 
-			/*
-			 * The datatype was found, so extract the data and log to the
-			 * requested filename. We need to open the file for appending
-			 * since the check might have already found the type in another
-			 * database earlier in the loop.
-			 */
-			if (ntups)
-			{
-				/*
-				 * Make sure we have a buffer to save reports to now that we
-				 * found a first failing check.
-				 */
-				if (!found)
-					initPQExpBuffer(&report);
-				found = true;
-
-				/*
-				 * If this is the first time we see an error for the check in
-				 * question then print a status message of the failure.
-				 */
-				if (!results[checknum])
-				{
-					pg_log(PG_REPORT, "    failed check: %s", _(cur_check->status));
-					appendPQExpBuffer(&report, "\n%s\n%s    %s\n",
-									  _(cur_check->report_text),
-									  _("A list of the problem columns is in the file:"),
-									  output_path);
-				}
-				results[checknum] = true;
-
-				i_nspname = PQfnumber(res, "nspname");
-				i_relname = PQfnumber(res, "relname");
-				i_attname = PQfnumber(res, "attname");
-
-				for (int rowno = 0; rowno < ntups; rowno++)
-				{
-					if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
-						pg_fatal("could not open file \"%s\": %m", output_path);
-
-					if (!db_used)
-					{
-						fprintf(script, "In database: %s\n", active_db->db_name);
-						db_used = true;
-					}
-					fprintf(script, "  %s.%s.%s\n",
-							PQgetvalue(res, rowno, i_nspname),
-							PQgetvalue(res, rowno, i_relname),
-							PQgetvalue(res, rowno, i_attname));
-				}
-
-				if (script)
-				{
-					fclose(script);
-					script = NULL;
-				}
-			}
+		queries[i] = data_type_check_query(i);
 
-			PQclear(res);
-		}
+		states[i].check = check;
+		states[i].result = &results[i];
+		states[i].report = &report;
 
-		PQfinish(conn);
+		upgrade_task_add_step(task, queries[i], process_data_type_check,
+							  true, &states[i]);
 	}
 
-	if (found)
-		pg_fatal("Data type checks failed: %s", report.data);
+	/*
+	 * Connect to each database in the cluster and run all defined checks
+	 * against that database before trying the next one.
+	 */
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
+
+	if (report)
+	{
+		pg_fatal("Data type checks failed: %s", report->data);
+		destroyPQExpBuffer(report);
+	}
 
 	pg_free(results);
+	for (int i = 0; i < n_data_types_usage_checks; i++)
+	{
+		if (queries[i])
+			pg_free(queries[i]);
+	}
+	pg_free(queries);
+	pg_free(states);
 
 	check_ok();
 }
@@ -616,7 +647,7 @@ check_and_dump_old_cluster(void)
 		check_old_cluster_subscription_state();
 	}
 
-	check_for_data_types_usage(&old_cluster, data_types_usage_checks);
+	check_for_data_types_usage(&old_cluster);
 
 	/*
 	 * PG 14 changed the function signature of encoding conversion functions.
-- 
2.39.3 (Apple Git-146)

v12-0007-Use-pg_upgrade-s-new-parallel-framework-for-isn-.patchtext/plain; charset=us-asciiDownload
From de03a379eacc7e30e241fe2e8fef5f4f7281fd3b Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:00:20 -0500
Subject: [PATCH v12 07/11] Use pg_upgrade's new parallel framework for isn and
 int8 check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 97 ++++++++++++++++++++------------------
 1 file changed, 50 insertions(+), 47 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index f935b53e1f..b8af7e541b 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1225,6 +1225,39 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 	check_ok();
 }
 
+/*
+ * Callback function for processing result of query for
+ * check_for_isn_and_int8_passing_mismatch()'s UpgradeTask.  If the query
+ * returned any rows (i.e., the check failed), write the details to the report
+ * file.
+ */
+static void
+process_isn_and_int8_passing_mismatch(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_proname = PQfnumber(res, "proname");
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+
+	AssertVariableIsOfType(&process_isn_and_int8_passing_mismatch,
+						   UpgradeTaskProcessCB);
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
+		{
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(report->file, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_proname));
+	}
+}
 
 /*
  *	check_for_isn_and_int8_passing_mismatch()
@@ -1236,9 +1269,13 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 static void
 check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTask *task;
+	UpgradeTaskReport report;
+	const char *query = "SELECT n.nspname, p.proname "
+		"FROM   pg_catalog.pg_proc p, "
+		"       pg_catalog.pg_namespace n "
+		"WHERE  p.pronamespace = n.oid AND "
+		"       p.probin = '$libdir/isn'";
 
 	prep_status("Checking for contrib/isn with bigint-passing mismatch");
 
@@ -1250,54 +1287,20 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 		return;
 	}
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
 			 log_opts.basedir,
 			 "contrib_isn_and_int8_pass_by_value.txt");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_proname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* Find any functions coming from contrib/isn */
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, p.proname "
-								"FROM	pg_catalog.pg_proc p, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	p.pronamespace = n.oid AND "
-								"		p.probin = '$libdir/isn'");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_proname = PQfnumber(res, "proname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_proname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	task = upgrade_task_create();
+	upgrade_task_add_step(task, query, process_isn_and_int8_passing_mismatch,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
 
-	if (script)
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains \"contrib/isn\" functions which rely on the\n"
 				 "bigint data type.  Your old and new clusters pass bigint values\n"
@@ -1305,7 +1308,7 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 				 "manually dump databases in the old cluster that use \"contrib/isn\"\n"
 				 "facilities, drop them, perform the upgrade, and then restore them.  A\n"
 				 "list of the problem functions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v12-0008-Use-pg_upgrade-s-new-parallel-framework-for-post.patchtext/plain; charset=us-asciiDownload
From 3c6c93cbe3db4efbeac4505099abdd7da58cb81c Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 15:10:19 -0500
Subject: [PATCH v12 08/11] Use pg_upgrade's new parallel framework for postfix
 operator check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 146 +++++++++++++++++++------------------
 1 file changed, 75 insertions(+), 71 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index b8af7e541b..28c4ddbca3 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1315,95 +1315,99 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 }
 
 /*
- * Verify that no user defined postfix operators exist.
+ * Callback function for processing result of query for
+ * check_for_user_defined_postfix_ops()'s UpgradeTask.  If the query returned
+ * any rows (i.e., the check failed), write the details to the report file.
  */
 static void
-check_for_user_defined_postfix_ops(ClusterInfo *cluster)
+process_user_defined_postfix_ops(DbInfo *dbinfo, PGresult *res, void *arg)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	int			ntups = PQntuples(res);
+	bool		db_used = false;
+	int			i_oproid = PQfnumber(res, "oproid");
+	int			i_oprnsp = PQfnumber(res, "oprnsp");
+	int			i_oprname = PQfnumber(res, "oprname");
+	int			i_typnsp = PQfnumber(res, "typnsp");
+	int			i_typname = PQfnumber(res, "typname");
 
-	prep_status("Checking for user-defined postfix operators");
+	AssertVariableIsOfType(&process_user_defined_postfix_ops,
+						   UpgradeTaskProcessCB);
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
-			 log_opts.basedir,
-			 "postfix_ops.txt");
+	if (!ntups)
+		return;
 
-	/* Find any user defined postfix operators */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int rowno = 0; rowno < ntups; rowno++)
 	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_oproid,
-					i_oprnsp,
-					i_oprname,
-					i_typnsp,
-					i_typname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT o.oid AS oproid, "
-								"       n.nspname AS oprnsp, "
-								"       o.oprname, "
-								"       tn.nspname AS typnsp, "
-								"       t.typname "
-								"FROM pg_catalog.pg_operator o, "
-								"     pg_catalog.pg_namespace n, "
-								"     pg_catalog.pg_type t, "
-								"     pg_catalog.pg_namespace tn "
-								"WHERE o.oprnamespace = n.oid AND "
-								"      o.oprleft = t.oid AND "
-								"      t.typnamespace = tn.oid AND "
-								"      o.oprright = 0 AND "
-								"      o.oid >= 16384");
-		ntups = PQntuples(res);
-		i_oproid = PQfnumber(res, "oproid");
-		i_oprnsp = PQfnumber(res, "oprnsp");
-		i_oprname = PQfnumber(res, "oprname");
-		i_typnsp = PQfnumber(res, "typnsp");
-		i_typname = PQfnumber(res, "typname");
-		for (rowno = 0; rowno < ntups; rowno++)
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
 		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
-					PQgetvalue(res, rowno, i_oproid),
-					PQgetvalue(res, rowno, i_oprnsp),
-					PQgetvalue(res, rowno, i_oprname),
-					PQgetvalue(res, rowno, i_typnsp),
-					PQgetvalue(res, rowno, i_typname));
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
 		}
+		fprintf(report->file, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
+				PQgetvalue(res, rowno, i_oproid),
+				PQgetvalue(res, rowno, i_oprnsp),
+				PQgetvalue(res, rowno, i_oprname),
+				PQgetvalue(res, rowno, i_typnsp),
+				PQgetvalue(res, rowno, i_typname));
+	}
+}
 
-		PQclear(res);
+/*
+ * Verify that no user defined postfix operators exist.
+ */
+static void
+check_for_user_defined_postfix_ops(ClusterInfo *cluster)
+{
+	UpgradeTaskReport report;
+	UpgradeTask *task = upgrade_task_create();
+	const char *query;
 
-		PQfinish(conn);
-	}
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	query = "SELECT o.oid AS oproid, "
+		"       n.nspname AS oprnsp, "
+		"       o.oprname, "
+		"       tn.nspname AS typnsp, "
+		"       t.typname "
+		"FROM pg_catalog.pg_operator o, "
+		"     pg_catalog.pg_namespace n, "
+		"     pg_catalog.pg_type t, "
+		"     pg_catalog.pg_namespace tn "
+		"WHERE o.oprnamespace = n.oid AND "
+		"      o.oprleft = t.oid AND "
+		"      t.typnamespace = tn.oid AND "
+		"      o.oprright = 0 AND "
+		"      o.oid >= 16384";
 
-	if (script)
+	prep_status("Checking for user-defined postfix operators");
+
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
+			 log_opts.basedir,
+			 "postfix_ops.txt");
+
+	upgrade_task_add_step(task, query, process_user_defined_postfix_ops,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
+
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains user-defined postfix operators, which are not\n"
 				 "supported anymore.  Consider dropping the postfix operators and replacing\n"
 				 "them with prefix operators or function calls.\n"
 				 "A list of user-defined postfix operators is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v12-0009-Use-pg_upgrade-s-new-parallel-framework-for-poly.patchtext/plain; charset=us-asciiDownload
From a99d4ec4aac119ff08e510831304327daa429494 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 15:21:29 -0500
Subject: [PATCH v12 09/11] Use pg_upgrade's new parallel framework for
 polymorphics check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 159 +++++++++++++++++++------------------
 1 file changed, 83 insertions(+), 76 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 28c4ddbca3..92a3aa6a77 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1413,6 +1413,40 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 		check_ok();
 }
 
+/*
+ * Callback function for processing results of query for
+ * check_for_incompatible_polymorphics()'s UpgradeTask.  If the query returned
+ * any rows (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_incompat_polymorphics(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_objkind = PQfnumber(res, "objkind");
+	int			i_objname = PQfnumber(res, "objname");
+
+	AssertVariableIsOfType(&process_incompat_polymorphics,
+						   UpgradeTaskProcessCB);
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
+		{
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+
+		fprintf(report->file, "  %s: %s\n",
+				PQgetvalue(res, rowno, i_objkind),
+				PQgetvalue(res, rowno, i_objname));
+	}
+}
+
 /*
  *	check_for_incompatible_polymorphics()
  *
@@ -1422,14 +1456,15 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 static void
 check_for_incompatible_polymorphics(ClusterInfo *cluster)
 {
-	PGresult   *res;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
 	PQExpBufferData old_polymorphics;
+	UpgradeTask *task = upgrade_task_create();
+	UpgradeTaskReport report;
+	char	   *query;
 
 	prep_status("Checking for incompatible polymorphic functions");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
 			 log_opts.basedir,
 			 "incompatible_polymorphics.txt");
 
@@ -1453,80 +1488,51 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 							 ", 'array_positions(anyarray,anyelement)'"
 							 ", 'width_bucket(anyelement,anyarray)'");
 
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		bool		db_used = false;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-		int			ntups;
-		int			i_objkind,
-					i_objname;
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-		/* Aggregate transition functions */
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Aggregate final functions */
-								"UNION ALL "
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Operators */
-								"UNION ALL "
-								"SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
-								"FROM pg_operator AS op "
-								"WHERE op.oid >= 16384 "
-								"AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
-								old_polymorphics.data,
-								old_polymorphics.data,
-								old_polymorphics.data);
-
-		ntups = PQntuples(res);
-
-		i_objkind = PQfnumber(res, "objkind");
-		i_objname = PQfnumber(res, "objname");
-
-		for (int rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-
-			fprintf(script, "  %s: %s\n",
-					PQgetvalue(res, rowno, i_objkind),
-					PQgetvalue(res, rowno, i_objname));
-		}
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
 
-		PQclear(res);
-		PQfinish(conn);
-	}
+	/* Aggregate transition functions */
+	query = psprintf("SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					 "FROM pg_proc AS p "
+					 "JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					 "JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
+					 "WHERE p.oid >= 16384 "
+					 "AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Aggregate final functions */
+					 "UNION ALL "
+					 "SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					 "FROM pg_proc AS p "
+					 "JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					 "JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
+					 "WHERE p.oid >= 16384 "
+					 "AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Operators */
+					 "UNION ALL "
+					 "SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
+					 "FROM pg_operator AS op "
+					 "WHERE op.oid >= 16384 "
+					 "AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
+					 old_polymorphics.data,
+					 old_polymorphics.data,
+					 old_polymorphics.data);
+
+	upgrade_task_add_step(task, query, process_incompat_polymorphics,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
 
-	if (script)
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains user-defined objects that refer to internal\n"
 				 "polymorphic functions with arguments of type \"anyarray\" or \"anyelement\".\n"
@@ -1534,12 +1540,13 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 				 "afterwards, changing them to refer to the new corresponding functions with\n"
 				 "arguments of type \"anycompatiblearray\" and \"anycompatible\".\n"
 				 "A list of the problematic objects is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
 
 	termPQExpBuffer(&old_polymorphics);
+	pg_free(query);
 }
 
 /*
-- 
2.39.3 (Apple Git-146)

v12-0010-Use-pg_upgrade-s-new-parallel-framework-for-WITH.patchtext/plain; charset=us-asciiDownload
From eb77f88c2edf68bacbb7e652da23fac72bdf56cd Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 15:27:37 -0500
Subject: [PATCH v12 10/11] Use pg_upgrade's new parallel framework for WITH
 OIDS check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 100 +++++++++++++++++++------------------
 1 file changed, 52 insertions(+), 48 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 92a3aa6a77..dff440b29a 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1550,72 +1550,76 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 }
 
 /*
- * Verify that no tables are declared WITH OIDS.
+ * Callback function for processing results of query for
+ * check_for_tables_with_oids()'s UpgradeTask.  If the query returned any rows
+ * (i.e., the check failed), write the details to the report file.
  */
 static void
-check_for_tables_with_oids(ClusterInfo *cluster)
+process_with_oids_check(DbInfo *dbinfo, PGresult *res, void *arg)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
 
-	prep_status("Checking for tables WITH OIDS");
+	AssertVariableIsOfType(&process_with_oids_check, UpgradeTaskProcessCB);
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
-			 log_opts.basedir,
-			 "tables_with_oids.txt");
+	if (!ntups)
+		return;
 
-	/* Find any tables declared WITH OIDS */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int rowno = 0; rowno < ntups; rowno++)
 	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_relname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, c.relname "
-								"FROM	pg_catalog.pg_class c, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	c.relnamespace = n.oid AND "
-								"		c.relhasoids AND"
-								"       n.nspname NOT IN ('pg_catalog')");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_relname = PQfnumber(res, "relname");
-		for (rowno = 0; rowno < ntups; rowno++)
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
 		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_relname));
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
 		}
+		fprintf(report->file, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_relname));
+	}
+}
 
-		PQclear(res);
+/*
+ * Verify that no tables are declared WITH OIDS.
+ */
+static void
+check_for_tables_with_oids(ClusterInfo *cluster)
+{
+	UpgradeTaskReport report;
+	UpgradeTask *task = upgrade_task_create();
+	const char *query = "SELECT n.nspname, c.relname "
+		"FROM   pg_catalog.pg_class c, "
+		"       pg_catalog.pg_namespace n "
+		"WHERE  c.relnamespace = n.oid AND "
+		"       c.relhasoids AND"
+		"       n.nspname NOT IN ('pg_catalog')";
 
-		PQfinish(conn);
-	}
+	prep_status("Checking for tables WITH OIDS");
 
-	if (script)
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
+			 log_opts.basedir,
+			 "tables_with_oids.txt");
+
+	upgrade_task_add_step(task, query, process_with_oids_check,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
+
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains tables declared WITH OIDS, which is not\n"
 				 "supported anymore.  Consider removing the oid column using\n"
 				 "    ALTER TABLE ... SET WITHOUT OIDS;\n"
 				 "A list of tables with the problem is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v12-0011-Use-pg_upgrade-s-parallel-framework-for-encoding.patchtext/plain; charset=us-asciiDownload
From f835d34c6dd490afa8e24fe9c72d3bc067877338 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 15:35:31 -0500
Subject: [PATCH v12 11/11] Use pg_upgrade's parallel framework for encoding
 conversion check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 120 ++++++++++++++++++++-----------------
 1 file changed, 64 insertions(+), 56 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index dff440b29a..01ab3d0694 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1684,81 +1684,89 @@ check_for_pg_role_prefix(ClusterInfo *cluster)
 }
 
 /*
- * Verify that no user-defined encoding conversions exist.
+ * Callback function for processing results of query for
+ * check_for_user_defined_encoding_conversions()'s UpgradeTask.  If the query
+ * returned any rows (i.e., the check failed), write the details to the report
+ * file.
  */
 static void
-check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
+process_user_defined_encoding_conversions(DbInfo *dbinfo, PGresult *res, void *arg)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_conoid = PQfnumber(res, "conoid");
+	int			i_conname = PQfnumber(res, "conname");
+	int			i_nspname = PQfnumber(res, "nspname");
 
-	prep_status("Checking for user-defined encoding conversions");
+	AssertVariableIsOfType(&process_user_defined_encoding_conversions,
+						   UpgradeTaskProcessCB);
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
-			 log_opts.basedir,
-			 "encoding_conversions.txt");
+	if (!ntups)
+		return;
 
-	/* Find any user defined encoding conversions */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int rowno = 0; rowno < ntups; rowno++)
 	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_conoid,
-					i_conname,
-					i_nspname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT c.oid as conoid, c.conname, n.nspname "
-								"FROM pg_catalog.pg_conversion c, "
-								"     pg_catalog.pg_namespace n "
-								"WHERE c.connamespace = n.oid AND "
-								"      c.oid >= 16384");
-		ntups = PQntuples(res);
-		i_conoid = PQfnumber(res, "conoid");
-		i_conname = PQfnumber(res, "conname");
-		i_nspname = PQfnumber(res, "nspname");
-		for (rowno = 0; rowno < ntups; rowno++)
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
 		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s\n",
-					PQgetvalue(res, rowno, i_conoid),
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_conname));
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
 		}
+		fprintf(report->file, "  (oid=%s) %s.%s\n",
+				PQgetvalue(res, rowno, i_conoid),
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_conname));
+	}
+}
 
-		PQclear(res);
+/*
+ * Verify that no user-defined encoding conversions exist.
+ */
+static void
+check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
+{
+	UpgradeTaskReport report;
+	UpgradeTask *task = upgrade_task_create();
+	const char *query;
 
-		PQfinish(conn);
-	}
+	prep_status("Checking for user-defined encoding conversions");
 
-	if (script)
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
+			 log_opts.basedir,
+			 "encoding_conversions.txt");
+
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	query = "SELECT c.oid as conoid, c.conname, n.nspname "
+		"FROM pg_catalog.pg_conversion c, "
+		"     pg_catalog.pg_namespace n "
+		"WHERE c.connamespace = n.oid AND "
+		"      c.oid >= 16384";
+
+	upgrade_task_add_step(task, query,
+						  process_user_defined_encoding_conversions,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
+
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains user-defined encoding conversions.\n"
 				 "The conversion function parameters changed in PostgreSQL version 14\n"
 				 "so this cluster cannot currently be upgraded.  You can remove the\n"
 				 "encoding conversions in the old cluster and restart the upgrade.\n"
 				 "A list of user-defined encoding conversions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

#32Ilya Gladyshev
ilya.v.gladyshev@gmail.com
In reply to: Nathan Bossart (#30)
Re: optimizing pg_upgrade's once-in-each-database steps

On 01.09.2024 22:05, Nathan Bossart wrote:

I think we can actually just use PQstatus() here. But furthermore, I think
the way I was initiating connections was completely bogus. IIUC before
calling PQconnectPoll() the first time, we should wait for a write
indicator from select(), and then we should only call PQconnectPoll() after
subsequent indicators from select(). After spending quite a bit of time
staring at the PQconnectPoll() code, I'm quite surprised I haven't seen any
problems thus far. If I had to guess, I'd say that either PQconnectPoll()
is more resilient than I think it is, or I've just gotten lucky because
pg_upgrade establishes connections quickly.

Good catch, I didn't look so closely at this.

Anyway, to fix this, I've added some more fields to the slot struct to
keep track of the information we need to properly establish connections,
and we now pay careful attention to the return value of select() so that we
know which slots are ready for processing. This seemed like a nice little
optimization independent of fixing connection establishment. I was worried
this was going to require a lot more code, but I think it only added ~50
lines or so.

The fix looks right to me, but I got confused by the skip_wait and this
`if`:

+            if (PQstatus(slot->conn) != CONNECTION_OK)
+                return;

This branch checks connection status that hasn't been refreshed after
the select. When we go back to wait_slots after this, PQconnectPoll will
refresh the connection status and run select with skip_wait=true, I
believe, we could simplify this by moving the PQconnectPoll back to the
process_slots, so that we can process connection right after polling, if
it's ready. Something like this:

diff --git a/src/bin/pg_upgrade/task.c b/src/bin/pg_upgrade/task.c
index 3618dc08ff..73e987febf 100644
--- a/src/bin/pg_upgrade/task.c
+++ b/src/bin/pg_upgrade/task.c
@@ -237,6 +237,8 @@ process_query_result(const ClusterInfo *cluster, 
UpgradeTaskSlot *slot,
  static void
  process_slot(const ClusterInfo *cluster, UpgradeTaskSlot *slot, const 
UpgradeTask *task)
  {
+    PostgresPollingStatusType status;
+
      if (!slot->ready)
          return;

@@ -260,26 +262,26 @@ process_slot(const ClusterInfo *cluster,
UpgradeTaskSlot *slot, const UpgradeTas
             start_conn(cluster, slot);

             return;
-
         case CONNECTING:

-            /* Check for connection failure. */
-            if (PQstatus(slot->conn) == CONNECTION_BAD)
-                pg_fatal("connection failure: %s", 
PQerrorMessage(slot->conn));
-
-            /* Check whether the connection is still establishing. */
-            if (PQstatus(slot->conn) != CONNECTION_OK)
-                return;
-
-            /*
-             * Move on to running/processing the queries in the task.
-             */
-            slot->state = RUNNING_QUERIES;
-            if (!PQsendQuery(slot->conn, task->queries->data))
+            status = PQconnectPoll(slot->conn);
+            if (status == PGRES_POLLING_READING)
+                slot->select_mode = true;
+            else if (status == PGRES_POLLING_WRITING)
+                slot->select_mode = false;
+            else if (status == PGRES_POLLING_FAILED)
                  pg_fatal("connection failure: %s", 
PQerrorMessage(slot->conn));
+            else
+            {
+                /*
+                 * Move on to running/processing the queries in the task.
+                 */
+                slot->state = RUNNING_QUERIES;
+                if (!PQsendQuery(slot->conn, task->queries->data))
+                    pg_fatal("connection failure: %s", 
PQerrorMessage(slot->conn));

+            }
             return;
-
         case RUNNING_QUERIES:

             /*
@@ -370,8 +372,6 @@ wait_on_slots(UpgradeTaskSlot *slots, int numslots)

      for (int i = 0; i < numslots; i++)
      {
-        PostgresPollingStatusType status;
-
          switch (slots[i].state)
          {
              case FREE:
@@ -386,33 +386,7 @@ wait_on_slots(UpgradeTaskSlot *slots, int numslots)
                  continue;
              case CONNECTING:
-
-                /*
-                 * Don't call PQconnectPoll() again for this slot until
-                 * select() tells us something is ready.  Be sure to 
use the
-                 * previous poll mode in this case.
-                 */
-                if (!slots[i].ready)
-                    break;
-
-                /*
-                 * If we are waiting for the connection to establish, 
choose
-                 * whether to wait for reading or for writing on the 
socket as
-                 * appropriate.  If neither apply, mark the slot as 
ready and
-                 * skip waiting so that it is handled ASAP (we assume this
-                 * means the connection is either bad or fully ready).
-                 */
-                status = PQconnectPoll(slots[i].conn);
-                if (status == PGRES_POLLING_READING)
-                    slots[i].select_mode = true;
-                else if (status == PGRES_POLLING_WRITING)
-                    slots[i].select_mode = false;
-                else
-                {
-                    slots[i].ready = true;
-                    skip_wait = true;
-                    continue;
-                }
+                /* All the slot metadata was already setup in 
process_slots() */

                 break;

--
2.43.0

skip_wait can be removed in this case as well.

This is up to you, I think the v12 is good and commitable in any case.

#33Nathan Bossart
nathandbossart@gmail.com
In reply to: Ilya Gladyshev (#32)
11 attachment(s)
Re: optimizing pg_upgrade's once-in-each-database steps

On Wed, Sep 04, 2024 at 12:28:23AM +0100, Ilya Gladyshev wrote:

The fix looks right to me, but I got confused by the skip_wait and this
`if`:

+����������� if (PQstatus(slot->conn) != CONNECTION_OK)
+��������������� return;

This branch checks connection status that hasn't been refreshed after the
select. When we go back to wait_slots after this, PQconnectPoll will refresh
the connection status and run select with skip_wait=true, I believe, we
could simplify this by moving the PQconnectPoll back to the process_slots,
so that we can process connection right after polling, if it's ready.

Ah, yes, that's a nice way to simplify things. I ended up just making it
process_slot()'s responsibility to set the correct select_mode, at which
point the logic in the switch statement in wait_on_slots() is sparse enough
that it seems better to convert it to a couple of short "if" statements.

--
nathan

Attachments:

v13-0001-Introduce-framework-for-parallelizing-various-pg.patchtext/plain; charset=us-asciiDownload
From ce0febd718cb5541031e1e9949ac199b091b9da0 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 10:45:59 -0500
Subject: [PATCH v13 01/11] Introduce framework for parallelizing various
 pg_upgrade tasks.

A number of pg_upgrade steps require connecting to every database
in the cluster and running the same query in each one.  When there
are many databases, these steps are particularly time-consuming,
especially since these steps are performed sequentially in a single
process.

This commit introduces a new framework that makes it easy to
parallelize most of these once-in-each-database tasks.
Specifically, it manages a simple state machine of slots and uses
libpq's asynchronous APIs to establish the connections and run the
queries.  The --jobs option is used to determine the number of
slots to use.  To use this new task framework, callers simply need
to provide the query and a callback function to process its
results, and the framework takes care of the rest.  A more complete
description is provided at the top of the new task.c file.

None of the eligible once-in-each-database tasks are converted to
use this new framework in this commit.  That will be done via
several follow-up commits.

Reviewed-by: Jeff Davis, Robert Haas, Daniel Gustafsson, Ilya Gladyshev, Corey Huinker
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 doc/src/sgml/ref/pgupgrade.sgml  |   6 +-
 src/bin/pg_upgrade/Makefile      |   1 +
 src/bin/pg_upgrade/meson.build   |   1 +
 src/bin/pg_upgrade/pg_upgrade.h  |  21 ++
 src/bin/pg_upgrade/task.c        | 446 +++++++++++++++++++++++++++++++
 src/tools/pgindent/typedefs.list |   5 +
 6 files changed, 477 insertions(+), 3 deletions(-)
 create mode 100644 src/bin/pg_upgrade/task.c

diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml
index 9877f2f01c..fc2d0ff845 100644
--- a/doc/src/sgml/ref/pgupgrade.sgml
+++ b/doc/src/sgml/ref/pgupgrade.sgml
@@ -118,7 +118,7 @@ PostgreSQL documentation
      <varlistentry>
       <term><option>-j <replaceable class="parameter">njobs</replaceable></option></term>
       <term><option>--jobs=<replaceable class="parameter">njobs</replaceable></option></term>
-      <listitem><para>number of simultaneous processes or threads to use
+      <listitem><para>number of simultaneous connections and processes/threads to use
       </para></listitem>
      </varlistentry>
 
@@ -587,8 +587,8 @@ NET STOP postgresql-&majorversion;
 
     <para>
      The <option>--jobs</option> option allows multiple CPU cores to be used
-     for copying/linking of files and to dump and restore database schemas
-     in parallel;  a good place to start is the maximum of the number of
+     for copying/linking of files, dumping and restoring database schemas
+     in parallel, etc.;  a good place to start is the maximum of the number of
      CPU cores and tablespaces.  This option can dramatically reduce the
      time to upgrade a multi-database server running on a multiprocessor
      machine.
diff --git a/src/bin/pg_upgrade/Makefile b/src/bin/pg_upgrade/Makefile
index bde91e2beb..f83d2b5d30 100644
--- a/src/bin/pg_upgrade/Makefile
+++ b/src/bin/pg_upgrade/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	relfilenumber.o \
 	server.o \
 	tablespace.o \
+	task.o \
 	util.o \
 	version.o
 
diff --git a/src/bin/pg_upgrade/meson.build b/src/bin/pg_upgrade/meson.build
index 9825fa3305..3d88419674 100644
--- a/src/bin/pg_upgrade/meson.build
+++ b/src/bin/pg_upgrade/meson.build
@@ -14,6 +14,7 @@ pg_upgrade_sources = files(
   'relfilenumber.c',
   'server.c',
   'tablespace.c',
+  'task.c',
   'util.c',
   'version.c',
 )
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index cdb6e2b759..53f693c2d4 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -494,3 +494,24 @@ void		parallel_transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr
 										  char *old_pgdata, char *new_pgdata,
 										  char *old_tablespace);
 bool		reap_child(bool wait_for_child);
+
+/* task.c */
+
+typedef void (*UpgradeTaskProcessCB) (DbInfo *dbinfo, PGresult *res, void *arg);
+
+/* struct definition is private to task.c */
+typedef struct UpgradeTask UpgradeTask;
+
+UpgradeTask *upgrade_task_create(void);
+void		upgrade_task_add_step(UpgradeTask *task, const char *query,
+								  UpgradeTaskProcessCB process_cb, bool free_result,
+								  void *arg);
+void		upgrade_task_run(const UpgradeTask *task, const ClusterInfo *cluster);
+void		upgrade_task_free(UpgradeTask *task);
+
+/* convenient type for common private data needed by several tasks */
+typedef struct
+{
+	FILE	   *file;
+	char		path[MAXPGPATH];
+} UpgradeTaskReport;
diff --git a/src/bin/pg_upgrade/task.c b/src/bin/pg_upgrade/task.c
new file mode 100644
index 0000000000..cb7f9f5a2d
--- /dev/null
+++ b/src/bin/pg_upgrade/task.c
@@ -0,0 +1,446 @@
+/*
+ * task.c
+ *		framework for parallelizing pg_upgrade's once-in-each-database tasks
+ *
+ * This framework provides an efficient way of running the various
+ * once-in-each-database tasks required by pg_upgrade.  Specifically, it
+ * parallelizes these tasks by managing a simple state machine of
+ * user_opts.jobs slots and using libpq's asynchronous APIs to establish the
+ * connections and run the queries.  Callers simply need to create a callback
+ * function and build/execute an UpgradeTask.  A simple example follows:
+ *
+ *		static void
+ *		my_process_cb(DbInfo *dbinfo, PGresult *res, void *arg)
+ *		{
+ *			for (int i = 0; i < PQntuples(res); i++)
+ *			{
+ *				... process results ...
+ *			}
+ *		}
+ *
+ *		void
+ *		my_task(ClusterInfo *cluster)
+ *		{
+ *			UpgradeTask *task = upgrade_task_create();
+ *
+ *			upgrade_task_add_step(task,
+ *								  "... query text ...",
+ *								  my_process_cb,
+ *								  true,		// let the task free the PGresult
+ *								  NULL);	// "arg" pointer for callback
+ *			upgrade_task_run(task, cluster);
+ *			upgrade_task_free(task);
+ *		}
+ *
+ * Note that multiple steps can be added to a given task.  When there are
+ * multiple steps, the task will run all of the steps consecutively in the same
+ * database connection before freeing the connection and moving on.  In other
+ * words, it only ever initiates one connection to each database in the
+ * cluster for a given run.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ * src/bin/pg_upgrade/task.c
+ */
+
+#include "postgres_fe.h"
+
+#include "common/connect.h"
+#include "fe_utils/string_utils.h"
+#include "pg_upgrade.h"
+
+/*
+ * dbs_complete stores the number of databases that we have completed
+ * processing.  When this value equals the number of databases in the cluster,
+ * the task is finished.
+ */
+static int	dbs_complete;
+
+/*
+ * dbs_processing stores the index of the next database in the cluster's array
+ * of databases that will be picked up for processing.  It will always be
+ * greater than or equal to dbs_complete.
+ */
+static int	dbs_processing;
+
+/*
+ * This struct stores the information for a single step of a task.  Note that
+ * the query string is stored in the "queries" PQExpBuffer for the UpgradeTask.
+ * All steps in a task are run in a single connection before moving on to the
+ * next database (which requires a new connection).
+ */
+typedef struct UpgradeTaskStep
+{
+	UpgradeTaskProcessCB process_cb;	/* processes the results of the query */
+	bool		free_result;	/* should we free the result? */
+	void	   *arg;			/* pointer passed to process_cb */
+} UpgradeTaskStep;
+
+/*
+ * This struct is a thin wrapper around an array of steps, i.e.,
+ * UpgradeTaskStep, plus a PQExpBuffer for all the query strings.
+ */
+typedef struct UpgradeTask
+{
+	UpgradeTaskStep *steps;
+	int			num_steps;
+	PQExpBuffer queries;
+} UpgradeTask;
+
+/*
+ * The different states for a parallel slot.
+ */
+typedef enum
+{
+	FREE,						/* slot available for use in a new database */
+	CONNECTING,					/* waiting for connection to be established */
+	RUNNING_QUERIES,			/* running/processing queries in the task */
+} UpgradeTaskSlotState;
+
+/*
+ * We maintain an array of user_opts.jobs slots to execute the task.
+ */
+typedef struct
+{
+	UpgradeTaskSlotState state; /* state of the slot */
+	int			db_idx;			/* index of the database assigned to slot */
+	int			step_idx;		/* index of the current step of task */
+	PGconn	   *conn;			/* current connection managed by slot */
+	bool		ready;			/* slot is ready for processing */
+	bool		select_mode;	/* select() mode: true->read, false->write */
+	int			sock;			/* file descriptor for connection's socket */
+} UpgradeTaskSlot;
+
+/*
+ * Initializes an UpgradeTask.
+ */
+UpgradeTask *
+upgrade_task_create(void)
+{
+	UpgradeTask *task = pg_malloc0(sizeof(UpgradeTask));
+
+	task->queries = createPQExpBuffer();
+
+	/* All tasks must first set a secure search_path. */
+	upgrade_task_add_step(task, ALWAYS_SECURE_SEARCH_PATH_SQL, NULL, true, NULL);
+
+	return task;
+}
+
+/*
+ * Frees all storage associated with an UpgradeTask.
+ */
+void
+upgrade_task_free(UpgradeTask *task)
+{
+	if (task->steps)
+		pg_free(task->steps);
+
+	destroyPQExpBuffer(task->queries);
+
+	pg_free(task);
+}
+
+/*
+ * Adds a step to an UpgradeTask.  The steps will be executed in each database
+ * in the order in which they are added.
+ *
+ *	task: task object that must have been initialized via upgrade_task_create()
+ *	query: the query text
+ *	process_cb: function that processes the results of the query
+ *	free_result: should we free the PGresult, or leave it to the caller?
+ *	arg: pointer to task-specific data that is passed to each callback
+ */
+void
+upgrade_task_add_step(UpgradeTask *task, const char *query,
+					  UpgradeTaskProcessCB process_cb, bool free_result,
+					  void *arg)
+{
+	UpgradeTaskStep *new_step;
+
+	task->steps = pg_realloc(task->steps,
+							 ++task->num_steps * sizeof(UpgradeTaskStep));
+
+	new_step = &task->steps[task->num_steps - 1];
+	new_step->process_cb = process_cb;
+	new_step->free_result = free_result;
+	new_step->arg = arg;
+
+	appendPQExpBuffer(task->queries, "%s;", query);
+}
+
+/*
+ * Build a connection string for the slot's current database and asynchronously
+ * start a new connection, but do not wait for the connection to be
+ * established.
+ */
+static void
+start_conn(const ClusterInfo *cluster, UpgradeTaskSlot *slot)
+{
+	PQExpBufferData conn_opts;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db_idx];
+
+	/* Build connection string with proper quoting */
+	initPQExpBuffer(&conn_opts);
+	appendPQExpBufferStr(&conn_opts, "dbname=");
+	appendConnStrVal(&conn_opts, dbinfo->db_name);
+	appendPQExpBufferStr(&conn_opts, " user=");
+	appendConnStrVal(&conn_opts, os_info.user);
+	appendPQExpBuffer(&conn_opts, " port=%d", cluster->port);
+	if (cluster->sockdir)
+	{
+		appendPQExpBufferStr(&conn_opts, " host=");
+		appendConnStrVal(&conn_opts, cluster->sockdir);
+	}
+
+	slot->conn = PQconnectStart(conn_opts.data);
+
+	if (!slot->conn)
+		pg_fatal("failed to create connection with connection string: \"%s\"",
+				 conn_opts.data);
+
+	termPQExpBuffer(&conn_opts);
+}
+
+/*
+ * Run the process_cb callback function to process the result of a query, and
+ * free the result if the caller indicated we should do so.
+ */
+static void
+process_query_result(const ClusterInfo *cluster, UpgradeTaskSlot *slot,
+					 const UpgradeTask *task)
+{
+	UpgradeTaskStep *steps = &task->steps[slot->step_idx];
+	UpgradeTaskProcessCB process_cb = steps->process_cb;
+	DbInfo	   *dbinfo = &cluster->dbarr.dbs[slot->db_idx];
+	PGresult   *res = PQgetResult(slot->conn);
+
+	if (PQstatus(slot->conn) == CONNECTION_BAD ||
+		(PQresultStatus(res) != PGRES_TUPLES_OK &&
+		 PQresultStatus(res) != PGRES_COMMAND_OK))
+		pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+
+	/*
+	 * We assume that a NULL process_cb callback function means there's
+	 * nothing to process.  This is primarily intended for the inital step in
+	 * every task that sets a safe search_path.
+	 */
+	if (process_cb)
+		(*process_cb) (dbinfo, res, steps->arg);
+
+	if (steps->free_result)
+		PQclear(res);
+}
+
+/*
+ * Advances the state machine for a given slot as necessary.
+ */
+static void
+process_slot(const ClusterInfo *cluster, UpgradeTaskSlot *slot, const UpgradeTask *task)
+{
+	PostgresPollingStatusType status;
+
+	if (!slot->ready)
+		return;
+
+	switch (slot->state)
+	{
+		case FREE:
+
+			/*
+			 * If all of the databases in the cluster have been processed or
+			 * are currently being processed by other slots, we are done.
+			 */
+			if (dbs_processing >= cluster->dbarr.ndbs)
+				return;
+
+			/*
+			 * Claim the next database in the cluster's array and initiate a
+			 * new connection.
+			 */
+			slot->db_idx = dbs_processing++;
+			slot->state = CONNECTING;
+			start_conn(cluster, slot);
+
+			return;
+
+		case CONNECTING:
+
+			/* Check for connection failure. */
+			status = PQconnectPoll(slot->conn);
+			if (status == PGRES_POLLING_FAILED)
+				pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+
+			/* Check whether the connection is still establishing. */
+			if (status != PGRES_POLLING_OK)
+			{
+				slot->select_mode = (status == PGRES_POLLING_READING);
+				return;
+			}
+
+			/*
+			 * Move on to running/processing the queries in the task.
+			 */
+			slot->state = RUNNING_QUERIES;
+			slot->select_mode = true;	/* wait until ready for reading */
+			if (!PQsendQuery(slot->conn, task->queries->data))
+				pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+
+			return;
+
+		case RUNNING_QUERIES:
+
+			/*
+			 * Consume any available data and clear the read-ready indicator
+			 * for the connection.
+			 */
+			if (!PQconsumeInput(slot->conn))
+				pg_fatal("connection failure: %s", PQerrorMessage(slot->conn));
+
+			/*
+			 * Process any results that are ready so that we can free up this
+			 * slot for another database as soon as possible.
+			 */
+			for (; slot->step_idx < task->num_steps; slot->step_idx++)
+			{
+				/* If no more results are available yet, move on. */
+				if (PQisBusy(slot->conn))
+					return;
+
+				process_query_result(cluster, slot, task);
+			}
+
+			/*
+			 * If we just finished processing the result of the last step in
+			 * the task, free the slot.  We recursively call this function on
+			 * the newly-freed slot so that we can start initiating the next
+			 * connection immediately instead of waiting for the next loop
+			 * through the slots.
+			 */
+			dbs_complete++;
+			PQfinish(slot->conn);
+			memset(slot, 0, sizeof(UpgradeTaskSlot));
+			slot->ready = true;
+
+			process_slot(cluster, slot, task);
+
+			return;
+	}
+}
+
+/*
+ * Returns -1 on error, else the number of ready descriptors.
+ */
+static int
+select_loop(int maxFd, fd_set *input, fd_set *output)
+{
+	fd_set		save_input = *input;
+	fd_set		save_output = *output;
+
+	if (maxFd == 0)
+		return 0;
+
+	for (;;)
+	{
+		int			i;
+
+		*input = save_input;
+		*output = save_output;
+
+		i = select(maxFd + 1, input, output, NULL, NULL);
+
+#ifndef WIN32
+		if (i < 0 && errno == EINTR)
+			continue;
+#else
+		if (i == SOCKET_ERROR && WSAGetLastError() == WSAEINTR)
+			continue;
+#endif
+		return i;
+	}
+}
+
+/*
+ * Wait on the slots to either finish connecting or to receive query results if
+ * possible.  This avoids a tight loop in upgrade_task_run().
+ */
+static void
+wait_on_slots(UpgradeTaskSlot *slots, int numslots)
+{
+	fd_set		input;
+	fd_set		output;
+	int			maxFd = 0;
+
+	FD_ZERO(&input);
+	FD_ZERO(&output);
+
+	for (int i = 0; i < numslots; i++)
+	{
+		/*
+		 * We assume the previous call to process_slot() handled everything
+		 * that was marked ready in the previous call to wait_on_slots(), if
+		 * any.
+		 */
+		slots[i].ready = false;
+
+		/*
+		 * This function should only ever see free slots as we are finishing
+		 * processing the last few databases, at which point we don't have any
+		 * databases left for them to process.  We'll never use these slots
+		 * again, so we can safely ignore them.
+		 */
+		if (slots[i].state == FREE)
+			continue;
+
+		/*
+		 * Add the socket to the set.
+		 */
+		slots[i].sock = PQsocket(slots[i].conn);
+		if (slots[i].sock < 0)
+			pg_fatal("invalid socket");
+		FD_SET(slots[i].sock, slots[i].select_mode ? &input : &output);
+		maxFd = Max(maxFd, slots[i].sock);
+	}
+
+	/*
+	 * If we found socket(s) to wait on, wait.
+	 */
+	if (select_loop(maxFd, &input, &output) == -1)
+		pg_fatal("select() failed: %m");
+
+	/*
+	 * Mark which sockets appear to be ready.
+	 */
+	for (int i = 0; i < numslots; i++)
+		slots[i].ready |= (FD_ISSET(slots[i].sock, &input) ||
+						   FD_ISSET(slots[i].sock, &output));
+}
+
+/*
+ * Runs all the steps of the task in every database in the cluster using
+ * user_opts.jobs parallel slots.
+ */
+void
+upgrade_task_run(const UpgradeTask *task, const ClusterInfo *cluster)
+{
+	int			jobs = Max(1, user_opts.jobs);
+	UpgradeTaskSlot *slots = pg_malloc0(sizeof(UpgradeTaskSlot) * jobs);
+
+	dbs_complete = 0;
+	dbs_processing = 0;
+
+	/*
+	 * Process every slot the first time round.
+	 */
+	for (int i = 0; i < jobs; i++)
+		slots[i].ready = true;
+
+	while (dbs_complete < cluster->dbarr.ndbs)
+	{
+		for (int i = 0; i < jobs; i++)
+			process_slot(cluster, &slots[i], task);
+
+		wait_on_slots(slots, jobs);
+	}
+
+	pg_free(slots);
+}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index df3f336bec..725863f9c8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3040,6 +3040,11 @@ UnresolvedTup
 UnresolvedTupData
 UpdateContext
 UpdateStmt
+UpgradeTask
+UpgradeTaskReport
+UpgradeTaskSlot
+UpgradeTaskSlotState
+UpgradeTaskStep
 UploadManifestCmd
 UpperRelationKind
 UpperUniquePath
-- 
2.39.3 (Apple Git-146)

v13-0002-Use-pg_upgrade-s-new-parallel-framework-for-subs.patchtext/plain; charset=us-asciiDownload
From 6a93f1ac9f8cebc075a44d7e4b423858270b6a3f Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 17:21:19 -0500
Subject: [PATCH v13 02/11] Use pg_upgrade's new parallel framework for
 subscription checks.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 206 ++++++++++++++++++++-----------------
 1 file changed, 111 insertions(+), 95 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 96adea41e9..f8160e0140 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1905,6 +1905,38 @@ check_old_cluster_for_valid_slots(void)
 	check_ok();
 }
 
+/*
+ * Callback function for processing results of query for
+ * check_old_cluster_subscription_state()'s UpgradeTask.  If the query returned
+ * any rows (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_old_sub_state_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	int			ntup = PQntuples(res);
+	int			i_srsubstate = PQfnumber(res, "srsubstate");
+	int			i_subname = PQfnumber(res, "subname");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+
+	AssertVariableIsOfType(&process_old_sub_state_check, UpgradeTaskProcessCB);
+
+	for (int i = 0; i < ntup; i++)
+	{
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+
+		fprintf(report->file, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
+				PQgetvalue(res, i, i_srsubstate),
+				dbinfo->db_name,
+				PQgetvalue(res, i, i_subname),
+				PQgetvalue(res, i, i_nspname),
+				PQgetvalue(res, i, i_relname));
+	}
+}
+
 /*
  * check_old_cluster_subscription_state()
  *
@@ -1915,115 +1947,99 @@ check_old_cluster_for_valid_slots(void)
 static void
 check_old_cluster_subscription_state(void)
 {
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTask *task = upgrade_task_create();
+	UpgradeTaskReport report;
+	const char *query;
+	PGresult   *res;
+	PGconn	   *conn;
 	int			ntup;
 
 	prep_status("Checking for subscription state");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
 			 log_opts.basedir,
 			 "subs_invalid.txt");
-	for (int dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
-
-		/* We need to check for pg_replication_origin only once. */
-		if (dbnum == 0)
-		{
-			/*
-			 * Check that all the subscriptions have their respective
-			 * replication origin.
-			 */
-			res = executeQueryOrDie(conn,
-									"SELECT d.datname, s.subname "
-									"FROM pg_catalog.pg_subscription s "
-									"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
-									"	ON o.roname = 'pg_' || s.oid "
-									"INNER JOIN pg_catalog.pg_database d "
-									"	ON d.oid = s.subdbid "
-									"WHERE o.roname IS NULL;");
-
-			ntup = PQntuples(res);
-			for (int i = 0; i < ntup; i++)
-			{
-				if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-					pg_fatal("could not open file \"%s\": %m", output_path);
-				fprintf(script, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
-						PQgetvalue(res, i, 0),
-						PQgetvalue(res, i, 1));
-			}
-			PQclear(res);
-		}
-
-		/*
-		 * We don't allow upgrade if there is a risk of dangling slot or
-		 * origin corresponding to initial sync after upgrade.
-		 *
-		 * A slot/origin not created yet refers to the 'i' (initialize) state,
-		 * while 'r' (ready) state refers to a slot/origin created previously
-		 * but already dropped. These states are supported for pg_upgrade. The
-		 * other states listed below are not supported:
-		 *
-		 * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state
-		 * would retain a replication slot, which could not be dropped by the
-		 * sync worker spawned after the upgrade because the subscription ID
-		 * used for the slot name won't match anymore.
-		 *
-		 * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state
-		 * would retain the replication origin when there is a failure in
-		 * tablesync worker immediately after dropping the replication slot in
-		 * the publisher.
-		 *
-		 * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on
-		 * a relation upgraded while in this state would expect an origin ID
-		 * with the OID of the subscription used before the upgrade, causing
-		 * it to fail.
-		 *
-		 * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and
-		 * SUBREL_STATE_UNKNOWN: These states are not stored in the catalog,
-		 * so we need not allow these states.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT r.srsubstate, s.subname, n.nspname, c.relname "
-								"FROM pg_catalog.pg_subscription_rel r "
-								"LEFT JOIN pg_catalog.pg_subscription s"
-								"	ON r.srsubid = s.oid "
-								"LEFT JOIN pg_catalog.pg_class c"
-								"	ON r.srrelid = c.oid "
-								"LEFT JOIN pg_catalog.pg_namespace n"
-								"	ON c.relnamespace = n.oid "
-								"WHERE r.srsubstate NOT IN ('i', 'r') "
-								"ORDER BY s.subname");
-
-		ntup = PQntuples(res);
-		for (int i = 0; i < ntup; i++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-
-			fprintf(script, "The table sync state \"%s\" is not allowed for database:\"%s\" subscription:\"%s\" schema:\"%s\" relation:\"%s\"\n",
-					PQgetvalue(res, i, 0),
-					active_db->db_name,
-					PQgetvalue(res, i, 1),
-					PQgetvalue(res, i, 2),
-					PQgetvalue(res, i, 3));
-		}
 
-		PQclear(res);
-		PQfinish(conn);
+	/*
+	 * Check that all the subscriptions have their respective replication
+	 * origin.  This check only needs to run once.
+	 */
+	conn = connectToServer(&old_cluster, old_cluster.dbarr.dbs[0].db_name);
+	res = executeQueryOrDie(conn,
+							"SELECT d.datname, s.subname "
+							"FROM pg_catalog.pg_subscription s "
+							"LEFT OUTER JOIN pg_catalog.pg_replication_origin o "
+							"	ON o.roname = 'pg_' || s.oid "
+							"INNER JOIN pg_catalog.pg_database d "
+							"	ON d.oid = s.subdbid "
+							"WHERE o.roname IS NULL;");
+	ntup = PQntuples(res);
+	for (int i = 0; i < ntup; i++)
+	{
+		if (report.file == NULL &&
+			(report.file = fopen_priv(report.path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report.path);
+		fprintf(report.file, "The replication origin is missing for database:\"%s\" subscription:\"%s\"\n",
+				PQgetvalue(res, i, 0),
+				PQgetvalue(res, i, 1));
 	}
+	PQclear(res);
+	PQfinish(conn);
 
-	if (script)
+	/*
+	 * We don't allow upgrade if there is a risk of dangling slot or origin
+	 * corresponding to initial sync after upgrade.
+	 *
+	 * A slot/origin not created yet refers to the 'i' (initialize) state,
+	 * while 'r' (ready) state refers to a slot/origin created previously but
+	 * already dropped. These states are supported for pg_upgrade. The other
+	 * states listed below are not supported:
+	 *
+	 * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state would
+	 * retain a replication slot, which could not be dropped by the sync
+	 * worker spawned after the upgrade because the subscription ID used for
+	 * the slot name won't match anymore.
+	 *
+	 * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state would
+	 * retain the replication origin when there is a failure in tablesync
+	 * worker immediately after dropping the replication slot in the
+	 * publisher.
+	 *
+	 * c) SUBREL_STATE_FINISHEDCOPY: A tablesync worker spawned to work on a
+	 * relation upgraded while in this state would expect an origin ID with
+	 * the OID of the subscription used before the upgrade, causing it to
+	 * fail.
+	 *
+	 * d) SUBREL_STATE_SYNCWAIT, SUBREL_STATE_CATCHUP and
+	 * SUBREL_STATE_UNKNOWN: These states are not stored in the catalog, so we
+	 * need not allow these states.
+	 */
+	query = "SELECT r.srsubstate, s.subname, n.nspname, c.relname "
+		"FROM pg_catalog.pg_subscription_rel r "
+		"LEFT JOIN pg_catalog.pg_subscription s"
+		"   ON r.srsubid = s.oid "
+		"LEFT JOIN pg_catalog.pg_class c"
+		"   ON r.srrelid = c.oid "
+		"LEFT JOIN pg_catalog.pg_namespace n"
+		"   ON c.relnamespace = n.oid "
+		"WHERE r.srsubstate NOT IN ('i', 'r') "
+		"ORDER BY s.subname";
+
+	upgrade_task_add_step(task, query, process_old_sub_state_check,
+						  true, &report);
+
+	upgrade_task_run(task, &old_cluster);
+	upgrade_task_free(task);
+
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains subscriptions without origin or having relations not in i (initialize) or r (ready) state.\n"
 				 "You can allow the initial sync to finish for all relations and then restart the upgrade.\n"
 				 "A list of the problematic subscriptions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v13-0003-Use-pg_upgrade-s-new-parallel-framework-to-get-r.patchtext/plain; charset=us-asciiDownload
From 82ea867e4a1f0c93a5d489ced07b1c9c28a4402d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Jun 2024 21:09:33 -0500
Subject: [PATCH v13 03/11] Use pg_upgrade's new parallel framework to get
 relation info.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/info.c | 296 ++++++++++++++++++++------------------
 1 file changed, 154 insertions(+), 142 deletions(-)

diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index d3c1e8918d..2bfc8dcfba 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -11,6 +11,7 @@
 
 #include "access/transam.h"
 #include "catalog/pg_class_d.h"
+#include "pqexpbuffer.h"
 #include "pg_upgrade.h"
 
 static void create_rel_filename_map(const char *old_data, const char *new_data,
@@ -22,12 +23,14 @@ static void report_unmatched_relation(const RelInfo *rel, const DbInfo *db,
 static void free_db_and_rel_infos(DbInfoArr *db_arr);
 static void get_template0_info(ClusterInfo *cluster);
 static void get_db_infos(ClusterInfo *cluster);
-static void get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo);
+static char *get_rel_infos_query(void);
+static void process_rel_infos(DbInfo *dbinfo, PGresult *res, void *arg);
 static void free_rel_infos(RelInfoArr *rel_arr);
 static void print_db_infos(DbInfoArr *db_arr);
 static void print_rel_infos(RelInfoArr *rel_arr);
 static void print_slot_infos(LogicalSlotInfoArr *slot_arr);
-static void get_old_cluster_logical_slot_infos(DbInfo *dbinfo);
+static char *get_old_cluster_logical_slot_infos_query(void);
+static void process_old_cluster_logical_slot_infos(DbInfo *dbinfo, PGresult *res, void *arg);
 
 
 /*
@@ -276,7 +279,9 @@ report_unmatched_relation(const RelInfo *rel, const DbInfo *db, bool is_new_db)
 void
 get_db_rel_and_slot_infos(ClusterInfo *cluster)
 {
-	int			dbnum;
+	UpgradeTask *task = upgrade_task_create();
+	char	   *rel_infos_query = NULL;
+	char	   *logical_slot_infos_query = NULL;
 
 	if (cluster->dbarr.dbs != NULL)
 		free_db_and_rel_infos(&cluster->dbarr);
@@ -284,15 +289,37 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster)
 	get_template0_info(cluster);
 	get_db_infos(cluster);
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	rel_infos_query = get_rel_infos_query();
+	upgrade_task_add_step(task,
+						  rel_infos_query,
+						  process_rel_infos,
+						  true, NULL);
+
+	/*
+	 * Logical slots are only carried over to the new cluster when the old
+	 * cluster is on PG17 or newer.  This is because before that the logical
+	 * slots are not saved at shutdown, so there is no guarantee that the
+	 * latest confirmed_flush_lsn is saved to disk which can lead to data
+	 * loss. It is still not guaranteed for manually created slots in PG17, so
+	 * subsequent checks done in check_old_cluster_for_valid_slots() would
+	 * raise a FATAL error if such slots are included.
+	 */
+	if (cluster == &old_cluster &&
+		GET_MAJOR_VERSION(cluster->major_version) > 1600)
 	{
-		DbInfo	   *pDbInfo = &cluster->dbarr.dbs[dbnum];
+		logical_slot_infos_query = get_old_cluster_logical_slot_infos_query();
+		upgrade_task_add_step(task,
+							  logical_slot_infos_query,
+							  process_old_cluster_logical_slot_infos,
+							  true, NULL);
+	}
 
-		get_rel_infos(cluster, pDbInfo);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
 
-		if (cluster == &old_cluster)
-			get_old_cluster_logical_slot_infos(pDbInfo);
-	}
+	pg_free(rel_infos_query);
+	if (logical_slot_infos_query)
+		pg_free(logical_slot_infos_query);
 
 	if (cluster == &old_cluster)
 		pg_log(PG_VERBOSE, "\nsource databases:");
@@ -431,40 +458,21 @@ get_db_infos(ClusterInfo *cluster)
 
 
 /*
- * get_rel_infos()
+ * get_rel_infos_query()
  *
- * gets the relinfos for all the user tables and indexes of the database
- * referred to by "dbinfo".
+ * Returns the query for retrieving the relation information for all the user
+ * tables and indexes in the database, for use by get_db_rel_and_slot_infos()'s
+ * UpgradeTask.
  *
- * Note: the resulting RelInfo array is assumed to be sorted by OID.
- * This allows later processing to match up old and new databases efficiently.
+ * Note: the result is assumed to be sorted by OID.  This allows later
+ * processing to match up old and new databases efficiently.
  */
-static void
-get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
+static char *
+get_rel_infos_query(void)
 {
-	PGconn	   *conn = connectToServer(cluster,
-									   dbinfo->db_name);
-	PGresult   *res;
-	RelInfo    *relinfos;
-	int			ntups;
-	int			relnum;
-	int			num_rels = 0;
-	char	   *nspname = NULL;
-	char	   *relname = NULL;
-	char	   *tablespace = NULL;
-	int			i_spclocation,
-				i_nspname,
-				i_relname,
-				i_reloid,
-				i_indtable,
-				i_toastheap,
-				i_relfilenumber,
-				i_reltablespace;
-	char		query[QUERY_ALLOC];
-	char	   *last_namespace = NULL,
-			   *last_tablespace = NULL;
+	PQExpBufferData query;
 
-	query[0] = '\0';			/* initialize query string to empty */
+	initPQExpBuffer(&query);
 
 	/*
 	 * Create a CTE that collects OIDs of regular user tables and matviews,
@@ -476,34 +484,34 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * output, so we have to copy that system table.  It's easiest to do that
 	 * by treating it as a user table.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "WITH regular_heap (reloid, indtable, toastheap) AS ( "
-			 "  SELECT c.oid, 0::oid, 0::oid "
-			 "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
-			 "         ON c.relnamespace = n.oid "
-			 "  WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
-			 CppAsString2(RELKIND_MATVIEW) ") AND "
+	appendPQExpBuffer(&query,
+					  "WITH regular_heap (reloid, indtable, toastheap) AS ( "
+					  "  SELECT c.oid, 0::oid, 0::oid "
+					  "  FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n "
+					  "         ON c.relnamespace = n.oid "
+					  "  WHERE relkind IN (" CppAsString2(RELKIND_RELATION) ", "
+					  CppAsString2(RELKIND_MATVIEW) ") AND "
 	/* exclude possible orphaned temp tables */
-			 "    ((n.nspname !~ '^pg_temp_' AND "
-			 "      n.nspname !~ '^pg_toast_temp_' AND "
-			 "      n.nspname NOT IN ('pg_catalog', 'information_schema', "
-			 "                        'binary_upgrade', 'pg_toast') AND "
-			 "      c.oid >= %u::pg_catalog.oid) OR "
-			 "     (n.nspname = 'pg_catalog' AND "
-			 "      relname IN ('pg_largeobject') ))), ",
-			 FirstNormalObjectId);
+					  "    ((n.nspname !~ '^pg_temp_' AND "
+					  "      n.nspname !~ '^pg_toast_temp_' AND "
+					  "      n.nspname NOT IN ('pg_catalog', 'information_schema', "
+					  "                        'binary_upgrade', 'pg_toast') AND "
+					  "      c.oid >= %u::pg_catalog.oid) OR "
+					  "     (n.nspname = 'pg_catalog' AND "
+					  "      relname IN ('pg_largeobject') ))), ",
+					  FirstNormalObjectId);
 
 	/*
 	 * Add a CTE that collects OIDs of toast tables belonging to the tables
 	 * selected by the regular_heap CTE.  (We have to do this separately
 	 * because the namespace-name rules above don't work for toast tables.)
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "  toast_heap (reloid, indtable, toastheap) AS ( "
-			 "  SELECT c.reltoastrelid, 0::oid, c.oid "
-			 "  FROM regular_heap JOIN pg_catalog.pg_class c "
-			 "      ON regular_heap.reloid = c.oid "
-			 "  WHERE c.reltoastrelid != 0), ");
+	appendPQExpBufferStr(&query,
+						 "  toast_heap (reloid, indtable, toastheap) AS ( "
+						 "  SELECT c.reltoastrelid, 0::oid, c.oid "
+						 "  FROM regular_heap JOIN pg_catalog.pg_class c "
+						 "      ON regular_heap.reloid = c.oid "
+						 "  WHERE c.reltoastrelid != 0), ");
 
 	/*
 	 * Add a CTE that collects OIDs of all valid indexes on the previously
@@ -511,53 +519,68 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 	 * Testing indisready is necessary in 9.2, and harmless in earlier/later
 	 * versions.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "  all_index (reloid, indtable, toastheap) AS ( "
-			 "  SELECT indexrelid, indrelid, 0::oid "
-			 "  FROM pg_catalog.pg_index "
-			 "  WHERE indisvalid AND indisready "
-			 "    AND indrelid IN "
-			 "        (SELECT reloid FROM regular_heap "
-			 "         UNION ALL "
-			 "         SELECT reloid FROM toast_heap)) ");
+	appendPQExpBufferStr(&query,
+						 "  all_index (reloid, indtable, toastheap) AS ( "
+						 "  SELECT indexrelid, indrelid, 0::oid "
+						 "  FROM pg_catalog.pg_index "
+						 "  WHERE indisvalid AND indisready "
+						 "    AND indrelid IN "
+						 "        (SELECT reloid FROM regular_heap "
+						 "         UNION ALL "
+						 "         SELECT reloid FROM toast_heap)) ");
 
 	/*
 	 * And now we can write the query that retrieves the data we want for each
 	 * heap and index relation.  Make sure result is sorted by OID.
 	 */
-	snprintf(query + strlen(query), sizeof(query) - strlen(query),
-			 "SELECT all_rels.*, n.nspname, c.relname, "
-			 "  c.relfilenode, c.reltablespace, "
-			 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
-			 "FROM (SELECT * FROM regular_heap "
-			 "      UNION ALL "
-			 "      SELECT * FROM toast_heap "
-			 "      UNION ALL "
-			 "      SELECT * FROM all_index) all_rels "
-			 "  JOIN pg_catalog.pg_class c "
-			 "      ON all_rels.reloid = c.oid "
-			 "  JOIN pg_catalog.pg_namespace n "
-			 "     ON c.relnamespace = n.oid "
-			 "  LEFT OUTER JOIN pg_catalog.pg_tablespace t "
-			 "     ON c.reltablespace = t.oid "
-			 "ORDER BY 1;");
-
-	res = executeQueryOrDie(conn, "%s", query);
-
-	ntups = PQntuples(res);
+	appendPQExpBufferStr(&query,
+						 "SELECT all_rels.*, n.nspname, c.relname, "
+						 "  c.relfilenode, c.reltablespace, "
+						 "  pg_catalog.pg_tablespace_location(t.oid) AS spclocation "
+						 "FROM (SELECT * FROM regular_heap "
+						 "      UNION ALL "
+						 "      SELECT * FROM toast_heap "
+						 "      UNION ALL "
+						 "      SELECT * FROM all_index) all_rels "
+						 "  JOIN pg_catalog.pg_class c "
+						 "      ON all_rels.reloid = c.oid "
+						 "  JOIN pg_catalog.pg_namespace n "
+						 "     ON c.relnamespace = n.oid "
+						 "  LEFT OUTER JOIN pg_catalog.pg_tablespace t "
+						 "     ON c.reltablespace = t.oid "
+						 "ORDER BY 1;");
+
+	return query.data;
+}
 
-	relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+/*
+ * Callback function for processing results of the query returned by
+ * get_rel_infos_query(), which is used for get_db_rel_and_slot_infos()'s
+ * UpgradeTask.  This function stores the relation information for later use.
+ */
+static void
+process_rel_infos(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	int			ntups = PQntuples(res);
+	RelInfo    *relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups);
+	int			i_reloid = PQfnumber(res, "reloid");
+	int			i_indtable = PQfnumber(res, "indtable");
+	int			i_toastheap = PQfnumber(res, "toastheap");
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
+	int			i_relfilenumber = PQfnumber(res, "relfilenode");
+	int			i_reltablespace = PQfnumber(res, "reltablespace");
+	int			i_spclocation = PQfnumber(res, "spclocation");
+	int			num_rels = 0;
+	char	   *nspname = NULL;
+	char	   *relname = NULL;
+	char	   *tablespace = NULL;
+	char	   *last_namespace = NULL;
+	char	   *last_tablespace = NULL;
 
-	i_reloid = PQfnumber(res, "reloid");
-	i_indtable = PQfnumber(res, "indtable");
-	i_toastheap = PQfnumber(res, "toastheap");
-	i_nspname = PQfnumber(res, "nspname");
-	i_relname = PQfnumber(res, "relname");
-	i_relfilenumber = PQfnumber(res, "relfilenode");
-	i_reltablespace = PQfnumber(res, "reltablespace");
-	i_spclocation = PQfnumber(res, "spclocation");
+	AssertVariableIsOfType(&process_rel_infos, UpgradeTaskProcessCB);
 
-	for (relnum = 0; relnum < ntups; relnum++)
+	for (int relnum = 0; relnum < ntups; relnum++)
 	{
 		RelInfo    *curr = &relinfos[num_rels++];
 
@@ -610,44 +633,22 @@ get_rel_infos(ClusterInfo *cluster, DbInfo *dbinfo)
 			/* A zero reltablespace oid indicates the database tablespace. */
 			curr->tablespace = dbinfo->db_tablespace;
 	}
-	PQclear(res);
-
-	PQfinish(conn);
 
 	dbinfo->rel_arr.rels = relinfos;
 	dbinfo->rel_arr.nrels = num_rels;
 }
 
 /*
- * get_old_cluster_logical_slot_infos()
- *
- * Gets the LogicalSlotInfos for all the logical replication slots of the
- * database referred to by "dbinfo". The status of each logical slot is gotten
- * here, but they are used at the checking phase. See
- * check_old_cluster_for_valid_slots().
+ * get_old_cluster_logical_slot_infos_query()
  *
- * Note: This function will not do anything if the old cluster is pre-PG17.
- * This is because before that the logical slots are not saved at shutdown, so
- * there is no guarantee that the latest confirmed_flush_lsn is saved to disk
- * which can lead to data loss. It is still not guaranteed for manually created
- * slots in PG17, so subsequent checks done in
- * check_old_cluster_for_valid_slots() would raise a FATAL error if such slots
- * are included.
+ * Returns the query for retrieving the logical slot information for all the
+ * logical replication slots in the database, for use by
+ * get_db_rel_and_slot_infos()'s UpgradeTask.  The status of each logical slot
+ * is checked in check_old_cluster_for_valid_slots().
  */
-static void
-get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
+static char *
+get_old_cluster_logical_slot_infos_query(void)
 {
-	PGconn	   *conn;
-	PGresult   *res;
-	LogicalSlotInfo *slotinfos = NULL;
-	int			num_slots;
-
-	/* Logical slots can be migrated since PG17. */
-	if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1600)
-		return;
-
-	conn = connectToServer(&old_cluster, dbinfo->db_name);
-
 	/*
 	 * Fetch the logical replication slot information. The check whether the
 	 * slot is considered caught up is done by an upgrade function. This
@@ -665,18 +666,32 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 	 * started and stopped several times causing any temporary slots to be
 	 * removed.
 	 */
-	res = executeQueryOrDie(conn, "SELECT slot_name, plugin, two_phase, failover, "
-							"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
-							"FROM pg_catalog.pg_replication_slots "
-							"WHERE slot_type = 'logical' AND "
-							"database = current_database() AND "
-							"temporary IS FALSE;",
-							user_opts.live_check ? "FALSE" :
-							"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
-							"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
-							"END)");
-
-	num_slots = PQntuples(res);
+	return psprintf("SELECT slot_name, plugin, two_phase, failover, "
+					"%s as caught_up, invalidation_reason IS NOT NULL as invalid "
+					"FROM pg_catalog.pg_replication_slots "
+					"WHERE slot_type = 'logical' AND "
+					"database = current_database() AND "
+					"temporary IS FALSE;",
+					user_opts.live_check ? "FALSE" :
+					"(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE "
+					"ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) "
+					"END)");
+}
+
+/*
+ * Callback function for processing results of the query returned by
+ * get_old_cluster_logical_slot_infos_query(), which is used for
+ * get_db_rel_and_slot_infos()'s UpgradeTask.  This function stores the logical
+ * slot information for later use.
+ */
+static void
+process_old_cluster_logical_slot_infos(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	LogicalSlotInfo *slotinfos = NULL;
+	int			num_slots = PQntuples(res);
+
+	AssertVariableIsOfType(&process_old_cluster_logical_slot_infos,
+						   UpgradeTaskProcessCB);
 
 	if (num_slots)
 	{
@@ -709,9 +724,6 @@ get_old_cluster_logical_slot_infos(DbInfo *dbinfo)
 		}
 	}
 
-	PQclear(res);
-	PQfinish(conn);
-
 	dbinfo->slot_arr.slots = slotinfos;
 	dbinfo->slot_arr.nslots = num_slots;
 }
-- 
2.39.3 (Apple Git-146)

v13-0004-Use-pg_upgrade-s-new-parallel-framework-to-get-l.patchtext/plain; charset=us-asciiDownload
From 9db186129034a43615d508c74e2f0494385262e7 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 13:32:33 -0500
Subject: [PATCH v13 04/11] Use pg_upgrade's new parallel framework to get
 loadable libraries.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/function.c | 71 ++++++++++++++++++++++-------------
 1 file changed, 45 insertions(+), 26 deletions(-)

diff --git a/src/bin/pg_upgrade/function.c b/src/bin/pg_upgrade/function.c
index 7e3abed098..0588347b49 100644
--- a/src/bin/pg_upgrade/function.c
+++ b/src/bin/pg_upgrade/function.c
@@ -42,6 +42,30 @@ library_name_compare(const void *p1, const void *p2)
 					  ((const LibraryInfo *) p2)->dbnum);
 }
 
+/*
+ * Private state for get_loadable_libraries()'s UpgradeTask.
+ */
+struct loadable_libraries_state
+{
+	PGresult  **ress;			/* results for each database */
+	int			totaltups;		/* number of tuples in all results */
+};
+
+/*
+ * Callback function for processing results of query for
+ * get_loadable_libraries()'s UpgradeTask.  This function stores the results
+ * for later use within get_loadable_libraries().
+ */
+static void
+process_loadable_libraries(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct loadable_libraries_state *state = (struct loadable_libraries_state *) arg;
+
+	AssertVariableIsOfType(&process_loadable_libraries, UpgradeTaskProcessCB);
+
+	state->ress[dbinfo - old_cluster.dbarr.dbs] = res;
+	state->totaltups += PQntuples(res);
+}
 
 /*
  * get_loadable_libraries()
@@ -54,47 +78,41 @@ library_name_compare(const void *p1, const void *p2)
 void
 get_loadable_libraries(void)
 {
-	PGresult  **ress;
 	int			totaltups;
 	int			dbnum;
 	int			n_libinfos;
+	UpgradeTask *task = upgrade_task_create();
+	struct loadable_libraries_state state;
+	char	   *query;
 
-	ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
-	totaltups = 0;
+	state.ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *));
+	state.totaltups = 0;
 
-	/* Fetch all library names, removing duplicates within each DB */
-	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
-	{
-		DbInfo	   *active_db = &old_cluster.dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(&old_cluster, active_db->db_name);
+	query = psprintf("SELECT DISTINCT probin "
+					 "FROM pg_catalog.pg_proc "
+					 "WHERE prolang = %u AND "
+					 "probin IS NOT NULL AND "
+					 "oid >= %u;",
+					 ClanguageId,
+					 FirstNormalObjectId);
 
-		/*
-		 * Fetch all libraries containing non-built-in C functions in this DB.
-		 */
-		ress[dbnum] = executeQueryOrDie(conn,
-										"SELECT DISTINCT probin "
-										"FROM pg_catalog.pg_proc "
-										"WHERE prolang = %u AND "
-										"probin IS NOT NULL AND "
-										"oid >= %u;",
-										ClanguageId,
-										FirstNormalObjectId);
-		totaltups += PQntuples(ress[dbnum]);
-
-		PQfinish(conn);
-	}
+	upgrade_task_add_step(task, query, process_loadable_libraries,
+						  false, &state);
+
+	upgrade_task_run(task, &old_cluster);
+	upgrade_task_free(task);
 
 	/*
 	 * Allocate memory for required libraries and logical replication output
 	 * plugins.
 	 */
-	n_libinfos = totaltups + count_old_cluster_logical_slots();
+	n_libinfos = state.totaltups + count_old_cluster_logical_slots();
 	os_info.libraries = (LibraryInfo *) pg_malloc(sizeof(LibraryInfo) * n_libinfos);
 	totaltups = 0;
 
 	for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++)
 	{
-		PGresult   *res = ress[dbnum];
+		PGresult   *res = state.ress[dbnum];
 		int			ntups;
 		int			rowno;
 		LogicalSlotInfoArr *slot_arr = &old_cluster.dbarr.dbs[dbnum].slot_arr;
@@ -129,7 +147,8 @@ get_loadable_libraries(void)
 		}
 	}
 
-	pg_free(ress);
+	pg_free(state.ress);
+	pg_free(query);
 
 	os_info.num_libraries = totaltups;
 }
-- 
2.39.3 (Apple Git-146)

v13-0005-Use-pg_upgrade-s-new-parallel-framework-for-exte.patchtext/plain; charset=us-asciiDownload
From 28fe26ffa0c3d4d7a7a46a9e2b1d3aeb5b44eebc Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 14:18:39 -0500
Subject: [PATCH v13 05/11] Use pg_upgrade's new parallel framework for
 extension updates.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/version.c | 94 +++++++++++++++++++-----------------
 1 file changed, 49 insertions(+), 45 deletions(-)

diff --git a/src/bin/pg_upgrade/version.c b/src/bin/pg_upgrade/version.c
index 2de6dffccd..5084b08805 100644
--- a/src/bin/pg_upgrade/version.c
+++ b/src/bin/pg_upgrade/version.c
@@ -139,6 +139,41 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 		check_ok();
 }
 
+/*
+ * Callback function for processing results of query for
+ * report_extension_updates()'s UpgradeTask.  If the query returned any rows,
+ * write the details to the report file.
+ */
+static void
+process_extension_updates(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_name = PQfnumber(res, "name");
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+
+	AssertVariableIsOfType(&process_extension_updates, UpgradeTaskProcessCB);
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
+		{
+			PQExpBufferData connectbuf;
+
+			initPQExpBuffer(&connectbuf);
+			appendPsqlMetaConnect(&connectbuf, dbinfo->db_name);
+			fputs(connectbuf.data, report->file);
+			termPQExpBuffer(&connectbuf);
+			db_used = true;
+		}
+		fprintf(report->file, "ALTER EXTENSION %s UPDATE;\n",
+				quote_identifier(PQgetvalue(res, rowno, i_name)));
+	}
+}
+
 /*
  * report_extension_updates()
  *	Report extensions that should be updated.
@@ -146,57 +181,26 @@ old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode)
 void
 report_extension_updates(ClusterInfo *cluster)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char	   *output_path = "update_extensions.sql";
+	UpgradeTaskReport report;
+	UpgradeTask *task = upgrade_task_create();
+	const char *query = "SELECT name "
+		"FROM pg_available_extensions "
+		"WHERE installed_version != default_version";
 
 	prep_status("Checking for extension updates");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_name;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* find extensions needing updates */
-		res = executeQueryOrDie(conn,
-								"SELECT name "
-								"FROM pg_available_extensions "
-								"WHERE installed_version != default_version"
-			);
+	report.file = NULL;
+	strcpy(report.path, "update_extensions.sql");
 
-		ntups = PQntuples(res);
-		i_name = PQfnumber(res, "name");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				PQExpBufferData connectbuf;
+	upgrade_task_add_step(task, query, process_extension_updates,
+						  true, &report);
 
-				initPQExpBuffer(&connectbuf);
-				appendPsqlMetaConnect(&connectbuf, active_db->db_name);
-				fputs(connectbuf.data, script);
-				termPQExpBuffer(&connectbuf);
-				db_used = true;
-			}
-			fprintf(script, "ALTER EXTENSION %s UPDATE;\n",
-					quote_identifier(PQgetvalue(res, rowno, i_name)));
-		}
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
 
-		PQclear(res);
-
-		PQfinish(conn);
-	}
-
-	if (script)
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		report_status(PG_REPORT, "notice");
 		pg_log(PG_REPORT, "\n"
 			   "Your installation contains extensions that should be updated\n"
@@ -204,7 +208,7 @@ report_extension_updates(ClusterInfo *cluster)
 			   "    %s\n"
 			   "when executed by psql by the database superuser will update\n"
 			   "these extensions.",
-			   output_path);
+			   report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v13-0006-Use-pg_upgrade-s-new-parallel-framework-for-data.patchtext/plain; charset=us-asciiDownload
From 7dff4c4729a767a1106606bc8689b3a8ddf04c03 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Sat, 6 Jul 2024 21:06:31 -0500
Subject: [PATCH v13 06/11] Use pg_upgrade's new parallel framework for data
 type checks.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 351 ++++++++++++++++++++-----------------
 1 file changed, 191 insertions(+), 160 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index f8160e0140..f935b53e1f 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -314,6 +314,147 @@ static DataTypesUsageChecks data_types_usage_checks[] =
 	}
 };
 
+/*
+ * Private state for check_for_data_types_usage()'s UpgradeTask.
+ */
+struct data_type_check_state
+{
+	DataTypesUsageChecks *check;	/* the check for this step */
+	bool	   *result;			/* true if check failed for any database */
+	PQExpBuffer *report;		/* buffer for report on failed checks */
+};
+
+/*
+ * Returns a palloc'd query string for the data type check, for use by
+ * check_for_data_types_usage()'s UpgradeTask.
+ */
+static char *
+data_type_check_query(int checknum)
+{
+	DataTypesUsageChecks *check = &data_types_usage_checks[checknum];
+
+	return psprintf("WITH RECURSIVE oids AS ( "
+	/* start with the type(s) returned by base_query */
+					"	%s "
+					"	UNION ALL "
+					"	SELECT * FROM ( "
+	/* inner WITH because we can only reference the CTE once */
+					"		WITH x AS (SELECT oid FROM oids) "
+	/* domains on any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
+					"			UNION ALL "
+	/* arrays over any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
+					"			UNION ALL "
+	/* composite types containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
+					"			WHERE t.typtype = 'c' AND "
+					"				  t.oid = c.reltype AND "
+					"				  c.oid = a.attrelid AND "
+					"				  NOT a.attisdropped AND "
+					"				  a.atttypid = x.oid "
+					"			UNION ALL "
+	/* ranges containing any type selected so far */
+					"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
+					"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
+					"	) foo "
+					") "
+	/* now look for stored columns of any such type */
+					"SELECT n.nspname, c.relname, a.attname "
+					"FROM	pg_catalog.pg_class c, "
+					"		pg_catalog.pg_namespace n, "
+					"		pg_catalog.pg_attribute a "
+					"WHERE	c.oid = a.attrelid AND "
+					"		NOT a.attisdropped AND "
+					"		a.atttypid IN (SELECT oid FROM oids) AND "
+					"		c.relkind IN ("
+					CppAsString2(RELKIND_RELATION) ", "
+					CppAsString2(RELKIND_MATVIEW) ", "
+					CppAsString2(RELKIND_INDEX) ") AND "
+					"		c.relnamespace = n.oid AND "
+	/* exclude possible orphaned temp tables */
+					"		n.nspname !~ '^pg_temp_' AND "
+					"		n.nspname !~ '^pg_toast_temp_' AND "
+	/* exclude system catalogs, too */
+					"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
+					check->base_query);
+}
+
+/*
+ * Callback function for processing results of queries for
+ * check_for_data_types_usage()'s UpgradeTask.  If the query returned any rows
+ * (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_data_type_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	struct data_type_check_state *state = (struct data_type_check_state *) arg;
+	int			ntups = PQntuples(res);
+
+	AssertVariableIsOfType(&process_data_type_check, UpgradeTaskProcessCB);
+
+	if (ntups)
+	{
+		char		output_path[MAXPGPATH];
+		int			i_nspname;
+		int			i_relname;
+		int			i_attname;
+		FILE	   *script = NULL;
+		bool		db_used = false;
+
+		snprintf(output_path, sizeof(output_path), "%s/%s",
+				 log_opts.basedir,
+				 state->check->report_filename);
+
+		/*
+		 * Make sure we have a buffer to save reports to now that we found a
+		 * first failing check.
+		 */
+		if (*state->report == NULL)
+			*state->report = createPQExpBuffer();
+
+		/*
+		 * If this is the first time we see an error for the check in question
+		 * then print a status message of the failure.
+		 */
+		if (!(*state->result))
+		{
+			pg_log(PG_REPORT, "    failed check: %s", _(state->check->status));
+			appendPQExpBuffer(*state->report, "\n%s\n%s    %s\n",
+							  _(state->check->report_text),
+							  _("A list of the problem columns is in the file:"),
+							  output_path);
+		}
+		*state->result = true;
+
+		i_nspname = PQfnumber(res, "nspname");
+		i_relname = PQfnumber(res, "relname");
+		i_attname = PQfnumber(res, "attname");
+
+		for (int rowno = 0; rowno < ntups; rowno++)
+		{
+			if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
+				pg_fatal("could not open file \"%s\": %m", output_path);
+
+			if (!db_used)
+			{
+				fprintf(script, "In database: %s\n", dbinfo->db_name);
+				db_used = true;
+			}
+			fprintf(script, "  %s.%s.%s\n",
+					PQgetvalue(res, rowno, i_nspname),
+					PQgetvalue(res, rowno, i_relname),
+					PQgetvalue(res, rowno, i_attname));
+		}
+
+		if (script)
+		{
+			fclose(script);
+			script = NULL;
+		}
+	}
+}
+
 /*
  * check_for_data_types_usage()
  *	Detect whether there are any stored columns depending on given type(s)
@@ -334,13 +475,15 @@ static DataTypesUsageChecks data_types_usage_checks[] =
  * there's no storage involved in a view.
  */
 static void
-check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
+check_for_data_types_usage(ClusterInfo *cluster)
 {
-	bool		found = false;
 	bool	   *results;
-	PQExpBufferData report;
-	DataTypesUsageChecks *tmp = checks;
+	PQExpBuffer report = NULL;
+	DataTypesUsageChecks *tmp = data_types_usage_checks;
 	int			n_data_types_usage_checks = 0;
+	UpgradeTask *task = upgrade_task_create();
+	char	  **queries = NULL;
+	struct data_type_check_state *states;
 
 	prep_status("Checking data type usage");
 
@@ -353,175 +496,63 @@ check_for_data_types_usage(ClusterInfo *cluster, DataTypesUsageChecks *checks)
 
 	/* Prepare an array to store the results of checks in */
 	results = pg_malloc0(sizeof(bool) * n_data_types_usage_checks);
+	queries = pg_malloc0(sizeof(char *) * n_data_types_usage_checks);
+	states = pg_malloc0(sizeof(struct data_type_check_state) * n_data_types_usage_checks);
 
-	/*
-	 * Connect to each database in the cluster and run all defined checks
-	 * against that database before trying the next one.
-	 */
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int i = 0; i < n_data_types_usage_checks; i++)
 	{
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
+		DataTypesUsageChecks *check = &data_types_usage_checks[i];
 
-		for (int checknum = 0; checknum < n_data_types_usage_checks; checknum++)
+		if (check->threshold_version == MANUAL_CHECK)
 		{
-			PGresult   *res;
-			int			ntups;
-			int			i_nspname;
-			int			i_relname;
-			int			i_attname;
-			FILE	   *script = NULL;
-			bool		db_used = false;
-			char		output_path[MAXPGPATH];
-			DataTypesUsageChecks *cur_check = &checks[checknum];
-
-			if (cur_check->threshold_version == MANUAL_CHECK)
-			{
-				Assert(cur_check->version_hook);
-
-				/*
-				 * Make sure that the check applies to the current cluster
-				 * version and skip if not. If no check hook has been defined
-				 * we run the check for all versions.
-				 */
-				if (!cur_check->version_hook(cluster))
-					continue;
-			}
-			else if (cur_check->threshold_version != ALL_VERSIONS)
-			{
-				if (GET_MAJOR_VERSION(cluster->major_version) > cur_check->threshold_version)
-					continue;
-			}
-			else
-				Assert(cur_check->threshold_version == ALL_VERSIONS);
-
-			snprintf(output_path, sizeof(output_path), "%s/%s",
-					 log_opts.basedir,
-					 cur_check->report_filename);
+			Assert(check->version_hook);
 
 			/*
-			 * The type(s) of interest might be wrapped in a domain, array,
-			 * composite, or range, and these container types can be nested
-			 * (to varying extents depending on server version, but that's not
-			 * of concern here).  To handle all these cases we need a
-			 * recursive CTE.
+			 * Make sure that the check applies to the current cluster version
+			 * and skip it if not.
 			 */
-			res = executeQueryOrDie(conn,
-									"WITH RECURSIVE oids AS ( "
-			/* start with the type(s) returned by base_query */
-									"	%s "
-									"	UNION ALL "
-									"	SELECT * FROM ( "
-			/* inner WITH because we can only reference the CTE once */
-									"		WITH x AS (SELECT oid FROM oids) "
-			/* domains on any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typbasetype = x.oid AND typtype = 'd' "
-									"			UNION ALL "
-			/* arrays over any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, x WHERE typelem = x.oid AND typtype = 'b' "
-									"			UNION ALL "
-			/* composite types containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_class c, pg_catalog.pg_attribute a, x "
-									"			WHERE t.typtype = 'c' AND "
-									"				  t.oid = c.reltype AND "
-									"				  c.oid = a.attrelid AND "
-									"				  NOT a.attisdropped AND "
-									"				  a.atttypid = x.oid "
-									"			UNION ALL "
-			/* ranges containing any type selected so far */
-									"			SELECT t.oid FROM pg_catalog.pg_type t, pg_catalog.pg_range r, x "
-									"			WHERE t.typtype = 'r' AND r.rngtypid = t.oid AND r.rngsubtype = x.oid"
-									"	) foo "
-									") "
-			/* now look for stored columns of any such type */
-									"SELECT n.nspname, c.relname, a.attname "
-									"FROM	pg_catalog.pg_class c, "
-									"		pg_catalog.pg_namespace n, "
-									"		pg_catalog.pg_attribute a "
-									"WHERE	c.oid = a.attrelid AND "
-									"		NOT a.attisdropped AND "
-									"		a.atttypid IN (SELECT oid FROM oids) AND "
-									"		c.relkind IN ("
-									CppAsString2(RELKIND_RELATION) ", "
-									CppAsString2(RELKIND_MATVIEW) ", "
-									CppAsString2(RELKIND_INDEX) ") AND "
-									"		c.relnamespace = n.oid AND "
-			/* exclude possible orphaned temp tables */
-									"		n.nspname !~ '^pg_temp_' AND "
-									"		n.nspname !~ '^pg_toast_temp_' AND "
-			/* exclude system catalogs, too */
-									"		n.nspname NOT IN ('pg_catalog', 'information_schema')",
-									cur_check->base_query);
-
-			ntups = PQntuples(res);
+			if (!check->version_hook(cluster))
+				continue;
+		}
+		else if (check->threshold_version != ALL_VERSIONS)
+		{
+			if (GET_MAJOR_VERSION(cluster->major_version) > check->threshold_version)
+				continue;
+		}
+		else
+			Assert(check->threshold_version == ALL_VERSIONS);
 
-			/*
-			 * The datatype was found, so extract the data and log to the
-			 * requested filename. We need to open the file for appending
-			 * since the check might have already found the type in another
-			 * database earlier in the loop.
-			 */
-			if (ntups)
-			{
-				/*
-				 * Make sure we have a buffer to save reports to now that we
-				 * found a first failing check.
-				 */
-				if (!found)
-					initPQExpBuffer(&report);
-				found = true;
-
-				/*
-				 * If this is the first time we see an error for the check in
-				 * question then print a status message of the failure.
-				 */
-				if (!results[checknum])
-				{
-					pg_log(PG_REPORT, "    failed check: %s", _(cur_check->status));
-					appendPQExpBuffer(&report, "\n%s\n%s    %s\n",
-									  _(cur_check->report_text),
-									  _("A list of the problem columns is in the file:"),
-									  output_path);
-				}
-				results[checknum] = true;
-
-				i_nspname = PQfnumber(res, "nspname");
-				i_relname = PQfnumber(res, "relname");
-				i_attname = PQfnumber(res, "attname");
-
-				for (int rowno = 0; rowno < ntups; rowno++)
-				{
-					if (script == NULL && (script = fopen_priv(output_path, "a")) == NULL)
-						pg_fatal("could not open file \"%s\": %m", output_path);
-
-					if (!db_used)
-					{
-						fprintf(script, "In database: %s\n", active_db->db_name);
-						db_used = true;
-					}
-					fprintf(script, "  %s.%s.%s\n",
-							PQgetvalue(res, rowno, i_nspname),
-							PQgetvalue(res, rowno, i_relname),
-							PQgetvalue(res, rowno, i_attname));
-				}
-
-				if (script)
-				{
-					fclose(script);
-					script = NULL;
-				}
-			}
+		queries[i] = data_type_check_query(i);
 
-			PQclear(res);
-		}
+		states[i].check = check;
+		states[i].result = &results[i];
+		states[i].report = &report;
 
-		PQfinish(conn);
+		upgrade_task_add_step(task, queries[i], process_data_type_check,
+							  true, &states[i]);
 	}
 
-	if (found)
-		pg_fatal("Data type checks failed: %s", report.data);
+	/*
+	 * Connect to each database in the cluster and run all defined checks
+	 * against that database before trying the next one.
+	 */
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
+
+	if (report)
+	{
+		pg_fatal("Data type checks failed: %s", report->data);
+		destroyPQExpBuffer(report);
+	}
 
 	pg_free(results);
+	for (int i = 0; i < n_data_types_usage_checks; i++)
+	{
+		if (queries[i])
+			pg_free(queries[i]);
+	}
+	pg_free(queries);
+	pg_free(states);
 
 	check_ok();
 }
@@ -616,7 +647,7 @@ check_and_dump_old_cluster(void)
 		check_old_cluster_subscription_state();
 	}
 
-	check_for_data_types_usage(&old_cluster, data_types_usage_checks);
+	check_for_data_types_usage(&old_cluster);
 
 	/*
 	 * PG 14 changed the function signature of encoding conversion functions.
-- 
2.39.3 (Apple Git-146)

v13-0007-Use-pg_upgrade-s-new-parallel-framework-for-isn-.patchtext/plain; charset=us-asciiDownload
From c461ad2d1d9243ceebb6f3d06a590acb302e1942 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jul 2024 21:00:20 -0500
Subject: [PATCH v13 07/11] Use pg_upgrade's new parallel framework for isn and
 int8 check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 97 ++++++++++++++++++++------------------
 1 file changed, 50 insertions(+), 47 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index f935b53e1f..b8af7e541b 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1225,6 +1225,39 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 	check_ok();
 }
 
+/*
+ * Callback function for processing result of query for
+ * check_for_isn_and_int8_passing_mismatch()'s UpgradeTask.  If the query
+ * returned any rows (i.e., the check failed), write the details to the report
+ * file.
+ */
+static void
+process_isn_and_int8_passing_mismatch(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_proname = PQfnumber(res, "proname");
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+
+	AssertVariableIsOfType(&process_isn_and_int8_passing_mismatch,
+						   UpgradeTaskProcessCB);
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
+		{
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+		fprintf(report->file, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_proname));
+	}
+}
 
 /*
  *	check_for_isn_and_int8_passing_mismatch()
@@ -1236,9 +1269,13 @@ check_for_prepared_transactions(ClusterInfo *cluster)
 static void
 check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTask *task;
+	UpgradeTaskReport report;
+	const char *query = "SELECT n.nspname, p.proname "
+		"FROM   pg_catalog.pg_proc p, "
+		"       pg_catalog.pg_namespace n "
+		"WHERE  p.pronamespace = n.oid AND "
+		"       p.probin = '$libdir/isn'";
 
 	prep_status("Checking for contrib/isn with bigint-passing mismatch");
 
@@ -1250,54 +1287,20 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 		return;
 	}
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
 			 log_opts.basedir,
 			 "contrib_isn_and_int8_pass_by_value.txt");
 
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_proname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/* Find any functions coming from contrib/isn */
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, p.proname "
-								"FROM	pg_catalog.pg_proc p, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	p.pronamespace = n.oid AND "
-								"		p.probin = '$libdir/isn'");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_proname = PQfnumber(res, "proname");
-		for (rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_proname));
-		}
-
-		PQclear(res);
-
-		PQfinish(conn);
-	}
+	task = upgrade_task_create();
+	upgrade_task_add_step(task, query, process_isn_and_int8_passing_mismatch,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
 
-	if (script)
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains \"contrib/isn\" functions which rely on the\n"
 				 "bigint data type.  Your old and new clusters pass bigint values\n"
@@ -1305,7 +1308,7 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 				 "manually dump databases in the old cluster that use \"contrib/isn\"\n"
 				 "facilities, drop them, perform the upgrade, and then restore them.  A\n"
 				 "list of the problem functions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v13-0008-Use-pg_upgrade-s-new-parallel-framework-for-post.patchtext/plain; charset=us-asciiDownload
From b64abe70d94b58ac3209a992e8a3ec2145bf62e6 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 15:10:19 -0500
Subject: [PATCH v13 08/11] Use pg_upgrade's new parallel framework for postfix
 operator check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 146 +++++++++++++++++++------------------
 1 file changed, 75 insertions(+), 71 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index b8af7e541b..28c4ddbca3 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1315,95 +1315,99 @@ check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster)
 }
 
 /*
- * Verify that no user defined postfix operators exist.
+ * Callback function for processing result of query for
+ * check_for_user_defined_postfix_ops()'s UpgradeTask.  If the query returned
+ * any rows (i.e., the check failed), write the details to the report file.
  */
 static void
-check_for_user_defined_postfix_ops(ClusterInfo *cluster)
+process_user_defined_postfix_ops(DbInfo *dbinfo, PGresult *res, void *arg)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	int			ntups = PQntuples(res);
+	bool		db_used = false;
+	int			i_oproid = PQfnumber(res, "oproid");
+	int			i_oprnsp = PQfnumber(res, "oprnsp");
+	int			i_oprname = PQfnumber(res, "oprname");
+	int			i_typnsp = PQfnumber(res, "typnsp");
+	int			i_typname = PQfnumber(res, "typname");
 
-	prep_status("Checking for user-defined postfix operators");
+	AssertVariableIsOfType(&process_user_defined_postfix_ops,
+						   UpgradeTaskProcessCB);
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
-			 log_opts.basedir,
-			 "postfix_ops.txt");
+	if (!ntups)
+		return;
 
-	/* Find any user defined postfix operators */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int rowno = 0; rowno < ntups; rowno++)
 	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_oproid,
-					i_oprnsp,
-					i_oprname,
-					i_typnsp,
-					i_typname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT o.oid AS oproid, "
-								"       n.nspname AS oprnsp, "
-								"       o.oprname, "
-								"       tn.nspname AS typnsp, "
-								"       t.typname "
-								"FROM pg_catalog.pg_operator o, "
-								"     pg_catalog.pg_namespace n, "
-								"     pg_catalog.pg_type t, "
-								"     pg_catalog.pg_namespace tn "
-								"WHERE o.oprnamespace = n.oid AND "
-								"      o.oprleft = t.oid AND "
-								"      t.typnamespace = tn.oid AND "
-								"      o.oprright = 0 AND "
-								"      o.oid >= 16384");
-		ntups = PQntuples(res);
-		i_oproid = PQfnumber(res, "oproid");
-		i_oprnsp = PQfnumber(res, "oprnsp");
-		i_oprname = PQfnumber(res, "oprname");
-		i_typnsp = PQfnumber(res, "typnsp");
-		i_typname = PQfnumber(res, "typname");
-		for (rowno = 0; rowno < ntups; rowno++)
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
 		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
-					PQgetvalue(res, rowno, i_oproid),
-					PQgetvalue(res, rowno, i_oprnsp),
-					PQgetvalue(res, rowno, i_oprname),
-					PQgetvalue(res, rowno, i_typnsp),
-					PQgetvalue(res, rowno, i_typname));
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
 		}
+		fprintf(report->file, "  (oid=%s) %s.%s (%s.%s, NONE)\n",
+				PQgetvalue(res, rowno, i_oproid),
+				PQgetvalue(res, rowno, i_oprnsp),
+				PQgetvalue(res, rowno, i_oprname),
+				PQgetvalue(res, rowno, i_typnsp),
+				PQgetvalue(res, rowno, i_typname));
+	}
+}
 
-		PQclear(res);
+/*
+ * Verify that no user defined postfix operators exist.
+ */
+static void
+check_for_user_defined_postfix_ops(ClusterInfo *cluster)
+{
+	UpgradeTaskReport report;
+	UpgradeTask *task = upgrade_task_create();
+	const char *query;
 
-		PQfinish(conn);
-	}
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	query = "SELECT o.oid AS oproid, "
+		"       n.nspname AS oprnsp, "
+		"       o.oprname, "
+		"       tn.nspname AS typnsp, "
+		"       t.typname "
+		"FROM pg_catalog.pg_operator o, "
+		"     pg_catalog.pg_namespace n, "
+		"     pg_catalog.pg_type t, "
+		"     pg_catalog.pg_namespace tn "
+		"WHERE o.oprnamespace = n.oid AND "
+		"      o.oprleft = t.oid AND "
+		"      t.typnamespace = tn.oid AND "
+		"      o.oprright = 0 AND "
+		"      o.oid >= 16384";
 
-	if (script)
+	prep_status("Checking for user-defined postfix operators");
+
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
+			 log_opts.basedir,
+			 "postfix_ops.txt");
+
+	upgrade_task_add_step(task, query, process_user_defined_postfix_ops,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
+
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains user-defined postfix operators, which are not\n"
 				 "supported anymore.  Consider dropping the postfix operators and replacing\n"
 				 "them with prefix operators or function calls.\n"
 				 "A list of user-defined postfix operators is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v13-0009-Use-pg_upgrade-s-new-parallel-framework-for-poly.patchtext/plain; charset=us-asciiDownload
From dfd4ca66cbea51753ab96cd0a598064836544fe5 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 15:21:29 -0500
Subject: [PATCH v13 09/11] Use pg_upgrade's new parallel framework for
 polymorphics check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 159 +++++++++++++++++++------------------
 1 file changed, 83 insertions(+), 76 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 28c4ddbca3..92a3aa6a77 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1413,6 +1413,40 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 		check_ok();
 }
 
+/*
+ * Callback function for processing results of query for
+ * check_for_incompatible_polymorphics()'s UpgradeTask.  If the query returned
+ * any rows (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_incompat_polymorphics(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_objkind = PQfnumber(res, "objkind");
+	int			i_objname = PQfnumber(res, "objname");
+
+	AssertVariableIsOfType(&process_incompat_polymorphics,
+						   UpgradeTaskProcessCB);
+
+	for (int rowno = 0; rowno < ntups; rowno++)
+	{
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
+		{
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
+		}
+
+		fprintf(report->file, "  %s: %s\n",
+				PQgetvalue(res, rowno, i_objkind),
+				PQgetvalue(res, rowno, i_objname));
+	}
+}
+
 /*
  *	check_for_incompatible_polymorphics()
  *
@@ -1422,14 +1456,15 @@ check_for_user_defined_postfix_ops(ClusterInfo *cluster)
 static void
 check_for_incompatible_polymorphics(ClusterInfo *cluster)
 {
-	PGresult   *res;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
 	PQExpBufferData old_polymorphics;
+	UpgradeTask *task = upgrade_task_create();
+	UpgradeTaskReport report;
+	char	   *query;
 
 	prep_status("Checking for incompatible polymorphic functions");
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
 			 log_opts.basedir,
 			 "incompatible_polymorphics.txt");
 
@@ -1453,80 +1488,51 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 							 ", 'array_positions(anyarray,anyelement)'"
 							 ", 'width_bucket(anyelement,anyarray)'");
 
-	for (int dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
-	{
-		bool		db_used = false;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-		int			ntups;
-		int			i_objkind,
-					i_objname;
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-		/* Aggregate transition functions */
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Aggregate final functions */
-								"UNION ALL "
-								"SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
-								"FROM pg_proc AS p "
-								"JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
-								"JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
-								"WHERE p.oid >= 16384 "
-								"AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
-
-		/* Operators */
-								"UNION ALL "
-								"SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
-								"FROM pg_operator AS op "
-								"WHERE op.oid >= 16384 "
-								"AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
-								"AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
-								old_polymorphics.data,
-								old_polymorphics.data,
-								old_polymorphics.data);
-
-		ntups = PQntuples(res);
-
-		i_objkind = PQfnumber(res, "objkind");
-		i_objname = PQfnumber(res, "objname");
-
-		for (int rowno = 0; rowno < ntups; rowno++)
-		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-
-			fprintf(script, "  %s: %s\n",
-					PQgetvalue(res, rowno, i_objkind),
-					PQgetvalue(res, rowno, i_objname));
-		}
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
 
-		PQclear(res);
-		PQfinish(conn);
-	}
+	/* Aggregate transition functions */
+	query = psprintf("SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					 "FROM pg_proc AS p "
+					 "JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					 "JOIN pg_proc AS transfn ON transfn.oid=a.aggtransfn "
+					 "WHERE p.oid >= 16384 "
+					 "AND a.aggtransfn = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Aggregate final functions */
+					 "UNION ALL "
+					 "SELECT 'aggregate' AS objkind, p.oid::regprocedure::text AS objname "
+					 "FROM pg_proc AS p "
+					 "JOIN pg_aggregate AS a ON a.aggfnoid=p.oid "
+					 "JOIN pg_proc AS finalfn ON finalfn.oid=a.aggfinalfn "
+					 "WHERE p.oid >= 16384 "
+					 "AND a.aggfinalfn = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND a.aggtranstype = ANY(ARRAY['anyarray', 'anyelement']::regtype[]) "
+
+	/* Operators */
+					 "UNION ALL "
+					 "SELECT 'operator' AS objkind, op.oid::regoperator::text AS objname "
+					 "FROM pg_operator AS op "
+					 "WHERE op.oid >= 16384 "
+					 "AND oprcode = ANY(ARRAY[%s]::regprocedure[]) "
+					 "AND oprleft = ANY(ARRAY['anyarray', 'anyelement']::regtype[]);",
+					 old_polymorphics.data,
+					 old_polymorphics.data,
+					 old_polymorphics.data);
+
+	upgrade_task_add_step(task, query, process_incompat_polymorphics,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
 
-	if (script)
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains user-defined objects that refer to internal\n"
 				 "polymorphic functions with arguments of type \"anyarray\" or \"anyelement\".\n"
@@ -1534,12 +1540,13 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 				 "afterwards, changing them to refer to the new corresponding functions with\n"
 				 "arguments of type \"anycompatiblearray\" and \"anycompatible\".\n"
 				 "A list of the problematic objects is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
 
 	termPQExpBuffer(&old_polymorphics);
+	pg_free(query);
 }
 
 /*
-- 
2.39.3 (Apple Git-146)

v13-0010-Use-pg_upgrade-s-new-parallel-framework-for-WITH.patchtext/plain; charset=us-asciiDownload
From d7a9c321bff0f3d46809c9abb2b7b43c1a241704 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 15:27:37 -0500
Subject: [PATCH v13 10/11] Use pg_upgrade's new parallel framework for WITH
 OIDS check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 100 +++++++++++++++++++------------------
 1 file changed, 52 insertions(+), 48 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 92a3aa6a77..dff440b29a 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1550,72 +1550,76 @@ check_for_incompatible_polymorphics(ClusterInfo *cluster)
 }
 
 /*
- * Verify that no tables are declared WITH OIDS.
+ * Callback function for processing results of query for
+ * check_for_tables_with_oids()'s UpgradeTask.  If the query returned any rows
+ * (i.e., the check failed), write the details to the report file.
  */
 static void
-check_for_tables_with_oids(ClusterInfo *cluster)
+process_with_oids_check(DbInfo *dbinfo, PGresult *res, void *arg)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_nspname = PQfnumber(res, "nspname");
+	int			i_relname = PQfnumber(res, "relname");
 
-	prep_status("Checking for tables WITH OIDS");
+	AssertVariableIsOfType(&process_with_oids_check, UpgradeTaskProcessCB);
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
-			 log_opts.basedir,
-			 "tables_with_oids.txt");
+	if (!ntups)
+		return;
 
-	/* Find any tables declared WITH OIDS */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int rowno = 0; rowno < ntups; rowno++)
 	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_nspname,
-					i_relname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		res = executeQueryOrDie(conn,
-								"SELECT n.nspname, c.relname "
-								"FROM	pg_catalog.pg_class c, "
-								"		pg_catalog.pg_namespace n "
-								"WHERE	c.relnamespace = n.oid AND "
-								"		c.relhasoids AND"
-								"       n.nspname NOT IN ('pg_catalog')");
-
-		ntups = PQntuples(res);
-		i_nspname = PQfnumber(res, "nspname");
-		i_relname = PQfnumber(res, "relname");
-		for (rowno = 0; rowno < ntups; rowno++)
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
 		{
-			if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  %s.%s\n",
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_relname));
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
 		}
+		fprintf(report->file, "  %s.%s\n",
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_relname));
+	}
+}
 
-		PQclear(res);
+/*
+ * Verify that no tables are declared WITH OIDS.
+ */
+static void
+check_for_tables_with_oids(ClusterInfo *cluster)
+{
+	UpgradeTaskReport report;
+	UpgradeTask *task = upgrade_task_create();
+	const char *query = "SELECT n.nspname, c.relname "
+		"FROM   pg_catalog.pg_class c, "
+		"       pg_catalog.pg_namespace n "
+		"WHERE  c.relnamespace = n.oid AND "
+		"       c.relhasoids AND"
+		"       n.nspname NOT IN ('pg_catalog')";
 
-		PQfinish(conn);
-	}
+	prep_status("Checking for tables WITH OIDS");
 
-	if (script)
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
+			 log_opts.basedir,
+			 "tables_with_oids.txt");
+
+	upgrade_task_add_step(task, query, process_with_oids_check,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
+
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains tables declared WITH OIDS, which is not\n"
 				 "supported anymore.  Consider removing the oid column using\n"
 				 "    ALTER TABLE ... SET WITHOUT OIDS;\n"
 				 "A list of tables with the problem is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

v13-0011-Use-pg_upgrade-s-parallel-framework-for-encoding.patchtext/plain; charset=us-asciiDownload
From 11661fdafb639ae957ee803bad8f22ed0becc4e0 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 28 Aug 2024 15:35:31 -0500
Subject: [PATCH v13 11/11] Use pg_upgrade's parallel framework for encoding
 conversion check.

Reviewed-by: FIXME
Discussion: https://postgr.es/m/20240516211638.GA1688936%40nathanxps13
---
 src/bin/pg_upgrade/check.c | 120 ++++++++++++++++++++-----------------
 1 file changed, 64 insertions(+), 56 deletions(-)

diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index dff440b29a..01ab3d0694 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -1684,81 +1684,89 @@ check_for_pg_role_prefix(ClusterInfo *cluster)
 }
 
 /*
- * Verify that no user-defined encoding conversions exist.
+ * Callback function for processing results of query for
+ * check_for_user_defined_encoding_conversions()'s UpgradeTask.  If the query
+ * returned any rows (i.e., the check failed), write the details to the report
+ * file.
  */
 static void
-check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
+process_user_defined_encoding_conversions(DbInfo *dbinfo, PGresult *res, void *arg)
 {
-	int			dbnum;
-	FILE	   *script = NULL;
-	char		output_path[MAXPGPATH];
+	UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+	bool		db_used = false;
+	int			ntups = PQntuples(res);
+	int			i_conoid = PQfnumber(res, "conoid");
+	int			i_conname = PQfnumber(res, "conname");
+	int			i_nspname = PQfnumber(res, "nspname");
 
-	prep_status("Checking for user-defined encoding conversions");
+	AssertVariableIsOfType(&process_user_defined_encoding_conversions,
+						   UpgradeTaskProcessCB);
 
-	snprintf(output_path, sizeof(output_path), "%s/%s",
-			 log_opts.basedir,
-			 "encoding_conversions.txt");
+	if (!ntups)
+		return;
 
-	/* Find any user defined encoding conversions */
-	for (dbnum = 0; dbnum < cluster->dbarr.ndbs; dbnum++)
+	for (int rowno = 0; rowno < ntups; rowno++)
 	{
-		PGresult   *res;
-		bool		db_used = false;
-		int			ntups;
-		int			rowno;
-		int			i_conoid,
-					i_conname,
-					i_nspname;
-		DbInfo	   *active_db = &cluster->dbarr.dbs[dbnum];
-		PGconn	   *conn = connectToServer(cluster, active_db->db_name);
-
-		/*
-		 * The query below hardcodes FirstNormalObjectId as 16384 rather than
-		 * interpolating that C #define into the query because, if that
-		 * #define is ever changed, the cutoff we want to use is the value
-		 * used by pre-version 14 servers, not that of some future version.
-		 */
-		res = executeQueryOrDie(conn,
-								"SELECT c.oid as conoid, c.conname, n.nspname "
-								"FROM pg_catalog.pg_conversion c, "
-								"     pg_catalog.pg_namespace n "
-								"WHERE c.connamespace = n.oid AND "
-								"      c.oid >= 16384");
-		ntups = PQntuples(res);
-		i_conoid = PQfnumber(res, "conoid");
-		i_conname = PQfnumber(res, "conname");
-		i_nspname = PQfnumber(res, "nspname");
-		for (rowno = 0; rowno < ntups; rowno++)
+		if (report->file == NULL &&
+			(report->file = fopen_priv(report->path, "w")) == NULL)
+			pg_fatal("could not open file \"%s\": %m", report->path);
+		if (!db_used)
 		{
-			if (script == NULL &&
-				(script = fopen_priv(output_path, "w")) == NULL)
-				pg_fatal("could not open file \"%s\": %m", output_path);
-			if (!db_used)
-			{
-				fprintf(script, "In database: %s\n", active_db->db_name);
-				db_used = true;
-			}
-			fprintf(script, "  (oid=%s) %s.%s\n",
-					PQgetvalue(res, rowno, i_conoid),
-					PQgetvalue(res, rowno, i_nspname),
-					PQgetvalue(res, rowno, i_conname));
+			fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+			db_used = true;
 		}
+		fprintf(report->file, "  (oid=%s) %s.%s\n",
+				PQgetvalue(res, rowno, i_conoid),
+				PQgetvalue(res, rowno, i_nspname),
+				PQgetvalue(res, rowno, i_conname));
+	}
+}
 
-		PQclear(res);
+/*
+ * Verify that no user-defined encoding conversions exist.
+ */
+static void
+check_for_user_defined_encoding_conversions(ClusterInfo *cluster)
+{
+	UpgradeTaskReport report;
+	UpgradeTask *task = upgrade_task_create();
+	const char *query;
 
-		PQfinish(conn);
-	}
+	prep_status("Checking for user-defined encoding conversions");
 
-	if (script)
+	report.file = NULL;
+	snprintf(report.path, sizeof(report.path), "%s/%s",
+			 log_opts.basedir,
+			 "encoding_conversions.txt");
+
+	/*
+	 * The query below hardcodes FirstNormalObjectId as 16384 rather than
+	 * interpolating that C #define into the query because, if that #define is
+	 * ever changed, the cutoff we want to use is the value used by
+	 * pre-version 14 servers, not that of some future version.
+	 */
+	query = "SELECT c.oid as conoid, c.conname, n.nspname "
+		"FROM pg_catalog.pg_conversion c, "
+		"     pg_catalog.pg_namespace n "
+		"WHERE c.connamespace = n.oid AND "
+		"      c.oid >= 16384";
+
+	upgrade_task_add_step(task, query,
+						  process_user_defined_encoding_conversions,
+						  true, &report);
+	upgrade_task_run(task, cluster);
+	upgrade_task_free(task);
+
+	if (report.file)
 	{
-		fclose(script);
+		fclose(report.file);
 		pg_log(PG_REPORT, "fatal");
 		pg_fatal("Your installation contains user-defined encoding conversions.\n"
 				 "The conversion function parameters changed in PostgreSQL version 14\n"
 				 "so this cluster cannot currently be upgraded.  You can remove the\n"
 				 "encoding conversions in the old cluster and restart the upgrade.\n"
 				 "A list of user-defined encoding conversions is in the file:\n"
-				 "    %s", output_path);
+				 "    %s", report.path);
 	}
 	else
 		check_ok();
-- 
2.39.3 (Apple Git-146)

#34Daniel Gustafsson
daniel@yesql.se
In reply to: Nathan Bossart (#33)
Re: optimizing pg_upgrade's once-in-each-database steps

I've read and tested through the latest version of this patchset and I think
it's ready to go in. The one concern I have is that tasks can exit(1) on libpq
errors tearing down perfectly functional connections without graceful shutdown.
Longer term I think it would make sense to add similar exit handler callbacks
to the ones in pg_dump for graceful cleanup of connections. However, in order
to keep goalposts in clear view I don't think this patch need to have it, but
it would be good to consider once in.

Spotted a small typo in the comments:

+ * nothing to process. This is primarily intended for the inital step in
s/inital/initial/

--
Daniel Gustafsson

#35Nathan Bossart
nathandbossart@gmail.com
In reply to: Daniel Gustafsson (#34)
Re: optimizing pg_upgrade's once-in-each-database steps

On Thu, Sep 05, 2024 at 01:32:34PM +0200, Daniel Gustafsson wrote:

I've read and tested through the latest version of this patchset and I think
it's ready to go in.

Thanks for reviewing. I'm aiming to commit it later this week.

The one concern I have is that tasks can exit(1) on libpq
errors tearing down perfectly functional connections without graceful shutdown.
Longer term I think it would make sense to add similar exit handler callbacks
to the ones in pg_dump for graceful cleanup of connections. However, in order
to keep goalposts in clear view I don't think this patch need to have it, but
it would be good to consider once in.

This did cross my mind. I haven't noticed any problems in my testing, and
it looks like there are several existing places in pg_upgrade that call
pg_fatal() with open connections, so I'm inclined to agree that this is a
nice follow-up task that needn't hold up this patch set.

Spotted a small typo in the comments:

+ * nothing to process. This is primarily intended for the inital step in
s/inital/initial/

Will fix.

--
nathan

#36Daniel Gustafsson
daniel@yesql.se
In reply to: Nathan Bossart (#35)
Re: optimizing pg_upgrade's once-in-each-database steps

On 9 Sep 2024, at 21:17, Nathan Bossart <nathandbossart@gmail.com> wrote:

On Thu, Sep 05, 2024 at 01:32:34PM +0200, Daniel Gustafsson wrote:

I've read and tested through the latest version of this patchset and I think
it's ready to go in.

Thanks for reviewing. I'm aiming to commit it later this week.

+1. Looking forward to seeing what all the pg_dump/pg_upgrade changes amount
to in speed improvement when combined.

The one concern I have is that tasks can exit(1) on libpq
errors tearing down perfectly functional connections without graceful shutdown.
Longer term I think it would make sense to add similar exit handler callbacks
to the ones in pg_dump for graceful cleanup of connections. However, in order
to keep goalposts in clear view I don't think this patch need to have it, but
it would be good to consider once in.

This did cross my mind. I haven't noticed any problems in my testing, and
it looks like there are several existing places in pg_upgrade that call
pg_fatal() with open connections, so I'm inclined to agree that this is a
nice follow-up task that needn't hold up this patch set.

It could perhaps be a good introductory task for a new contributor who want a
fairly confined project to hack on.

--
Daniel Gustafsson

#37Nathan Bossart
nathandbossart@gmail.com
In reply to: Daniel Gustafsson (#36)
Re: optimizing pg_upgrade's once-in-each-database steps

On Mon, Sep 09, 2024 at 11:20:28PM +0200, Daniel Gustafsson wrote:

On 9 Sep 2024, at 21:17, Nathan Bossart <nathandbossart@gmail.com> wrote:

On Thu, Sep 05, 2024 at 01:32:34PM +0200, Daniel Gustafsson wrote:

I've read and tested through the latest version of this patchset and I think
it's ready to go in.

Thanks for reviewing. I'm aiming to commit it later this week.

+1. Looking forward to seeing what all the pg_dump/pg_upgrade changes amount
to in speed improvement when combined.

Committed.

--
nathan